diff --git a/core/backends/lan/lanlinkprovider.cpp b/core/backends/lan/lanlinkprovider.cpp index 86730b3a..82d45a1b 100644 --- a/core/backends/lan/lanlinkprovider.cpp +++ b/core/backends/lan/lanlinkprovider.cpp @@ -1,541 +1,541 @@ /** * Copyright 2013 Albert Vaca * * 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 . */ #include "lanlinkprovider.h" #include "core_debug.h" #ifndef Q_OS_WIN #include #include #include #include #endif #include #include #include #include #include #include #include #include #include "daemon.h" #include "landevicelink.h" #include "lanpairinghandler.h" #include "kdeconnectconfig.h" #define MIN_VERSION_WITH_SSL_SUPPORT 6 LanLinkProvider::LanLinkProvider(bool testMode) : m_testMode(testMode) { m_tcpPort = 0; m_combineBroadcastsTimer.setInterval(0); // increase this if waiting a single event-loop iteration is not enough m_combineBroadcastsTimer.setSingleShot(true); connect(&m_combineBroadcastsTimer, &QTimer::timeout, this, &LanLinkProvider::broadcastToNetwork); connect(&m_udpSocket, &QIODevice::readyRead, this, &LanLinkProvider::newUdpConnection); m_server = new Server(this); m_server->setProxy(QNetworkProxy::NoProxy); connect(m_server,&QTcpServer::newConnection,this, &LanLinkProvider::newConnection); m_udpSocket.setProxy(QNetworkProxy::NoProxy); //Detect when a network interface changes status, so we announce ourelves in the new network QNetworkConfigurationManager* networkManager = new QNetworkConfigurationManager(this); connect(networkManager, &QNetworkConfigurationManager::configurationChanged, this, &LanLinkProvider::onNetworkConfigurationChanged); } void LanLinkProvider::onNetworkConfigurationChanged(const QNetworkConfiguration& config) { if (m_lastConfig != config && config.state() == QNetworkConfiguration::Active) { m_lastConfig = config; onNetworkChange(); } } LanLinkProvider::~LanLinkProvider() { } void LanLinkProvider::onStart() { const QHostAddress bindAddress = m_testMode? QHostAddress::LocalHost : QHostAddress::Any; bool success = m_udpSocket.bind(bindAddress, UDP_PORT, QUdpSocket::ShareAddress); Q_ASSERT(success); qCDebug(KDECONNECT_CORE) << "onStart"; m_tcpPort = MIN_TCP_PORT; while (!m_server->listen(bindAddress, m_tcpPort)) { m_tcpPort++; if (m_tcpPort > MAX_TCP_PORT) { //No ports available? qCritical(KDECONNECT_CORE) << "Error opening a port in range" << MIN_TCP_PORT << "-" << MAX_TCP_PORT; m_tcpPort = 0; return; } } onNetworkChange(); } void LanLinkProvider::onStop() { qCDebug(KDECONNECT_CORE) << "onStop"; m_udpSocket.close(); m_server->close(); } void LanLinkProvider::onNetworkChange() { if (m_combineBroadcastsTimer.isActive()) { qCDebug(KDECONNECT_CORE()) << "Preventing duplicate broadcasts"; return; } m_combineBroadcastsTimer.start(); } //I'm in a new network, let's be polite and introduce myself void LanLinkProvider::broadcastToNetwork() { if (!m_server->isListening()) { //Not started return; } Q_ASSERT(m_tcpPort != 0); qCDebug(KDECONNECT_CORE()) << "Broadcasting identity packet"; QHostAddress destAddress = m_testMode? QHostAddress::LocalHost : QHostAddress(QStringLiteral("255.255.255.255")); NetworkPacket np(QLatin1String("")); NetworkPacket::createIdentityPacket(&np); np.set(QStringLiteral("tcpPort"), m_tcpPort); #ifdef Q_OS_WIN //On Windows we need to broadcast from every local IP address to reach all networks QUdpSocket sendSocket; sendSocket.setProxy(QNetworkProxy::NoProxy); for (const QNetworkInterface& iface : QNetworkInterface::allInterfaces()) { if ( (iface.flags() & QNetworkInterface::IsUp) && (iface.flags() & QNetworkInterface::IsRunning) && (iface.flags() & QNetworkInterface::CanBroadcast)) { for (const QNetworkAddressEntry& ifaceAddress : iface.addressEntries()) { QHostAddress sourceAddress = ifaceAddress.ip(); if (sourceAddress.protocol() == QAbstractSocket::IPv4Protocol && sourceAddress != QHostAddress::LocalHost) { qCDebug(KDECONNECT_CORE()) << "Broadcasting as" << sourceAddress; sendSocket.bind(sourceAddress, UDP_PORT); sendSocket.writeDatagram(np.serialize(), destAddress, UDP_PORT); sendSocket.close(); } } } } #else m_udpSocket.writeDatagram(np.serialize(), destAddress, UDP_PORT); #endif } //I'm the existing device, a new device is kindly introducing itself. //I will create a TcpSocket and try to connect. This can result in either connected() or connectError(). void LanLinkProvider::newUdpConnection() //udpBroadcastReceived { while (m_udpSocket.hasPendingDatagrams()) { QByteArray datagram; datagram.resize(m_udpSocket.pendingDatagramSize()); QHostAddress sender; m_udpSocket.readDatagram(datagram.data(), datagram.size(), &sender); if (sender.isLoopback() && !m_testMode) continue; NetworkPacket* receivedPacket = new NetworkPacket(QLatin1String("")); bool success = NetworkPacket::unserialize(datagram, receivedPacket); //qCDebug(KDECONNECT_CORE) << "udp connection from " << receivedPacket->; //qCDebug(KDECONNECT_CORE) << "Datagram " << datagram.data() ; if (!success || receivedPacket->type() != PACKET_TYPE_IDENTITY) { delete receivedPacket; continue; } if (receivedPacket->get(QStringLiteral("deviceId")) == KdeConnectConfig::instance()->deviceId()) { //qCDebug(KDECONNECT_CORE) << "Ignoring my own broadcast"; delete receivedPacket; continue; } int tcpPort = receivedPacket->get(QStringLiteral("tcpPort")); //qCDebug(KDECONNECT_CORE) << "Received Udp identity packet from" << sender << " asking for a tcp connection on port " << tcpPort; QSslSocket* socket = new QSslSocket(this); socket->setProxy(QNetworkProxy::NoProxy); m_receivedIdentityPackets[socket].np = receivedPacket; m_receivedIdentityPackets[socket].sender = sender; connect(socket, &QAbstractSocket::connected, this, &LanLinkProvider::connected); connect(socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(connectError())); socket->connectToHost(sender, tcpPort); } } void LanLinkProvider::connectError() { QSslSocket* socket = qobject_cast(sender()); if (!socket) return; disconnect(socket, &QAbstractSocket::connected, this, &LanLinkProvider::connected); disconnect(socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(connectError())); qCDebug(KDECONNECT_CORE) << "Fallback (1), try reverse connection (send udp packet)" << socket->errorString(); NetworkPacket np(QLatin1String("")); NetworkPacket::createIdentityPacket(&np); np.set(QStringLiteral("tcpPort"), m_tcpPort); m_udpSocket.writeDatagram(np.serialize(), m_receivedIdentityPackets[socket].sender, UDP_PORT); //The socket we created didn't work, and we didn't manage //to create a LanDeviceLink from it, deleting everything. delete m_receivedIdentityPackets.take(socket).np; delete socket; } //We received a UDP packet and answered by connecting to them by TCP. This gets called on a successful connection. void LanLinkProvider::connected() { QSslSocket* socket = qobject_cast(sender()); if (!socket) return; disconnect(socket, &QAbstractSocket::connected, this, &LanLinkProvider::connected); disconnect(socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(connectError())); configureSocket(socket); // If socket disconnects due to any reason after connection, link on ssl failure connect(socket, &QAbstractSocket::disconnected, socket, &QObject::deleteLater); NetworkPacket* receivedPacket = m_receivedIdentityPackets[socket].np; const QString& deviceId = receivedPacket->get(QStringLiteral("deviceId")); //qCDebug(KDECONNECT_CORE) << "Connected" << socket->isWritable(); // If network is on ssl, do not believe when they are connected, believe when handshake is completed NetworkPacket np2(QLatin1String("")); NetworkPacket::createIdentityPacket(&np2); socket->write(np2.serialize()); bool success = socket->waitForBytesWritten(); if (success) { qCDebug(KDECONNECT_CORE) << "TCP connection done (i'm the existing device)"; // if ssl supported if (receivedPacket->get(QStringLiteral("protocolVersion")) >= MIN_VERSION_WITH_SSL_SUPPORT) { bool isDeviceTrusted = KdeConnectConfig::instance()->trustedDevices().contains(deviceId); configureSslSocket(socket, deviceId, isDeviceTrusted); qCDebug(KDECONNECT_CORE) << "Starting server ssl (I'm the client TCP socket)"; connect(socket, &QSslSocket::encrypted, this, &LanLinkProvider::encrypted); if (isDeviceTrusted) { connect(socket, SIGNAL(sslErrors(QList)), this, SLOT(sslErrors(QList))); } socket->startServerEncryption(); return; // Return statement prevents from deleting received packet, needed in slot "encrypted" } else { qWarning() << receivedPacket->get(QStringLiteral("deviceName")) << "uses an old protocol version, this won't work"; //addLink(deviceId, socket, receivedPacket, LanDeviceLink::Remotely); } } else { //I think this will never happen, but if it happens the deviceLink //(or the socket that is now inside it) might not be valid. Delete them. qCDebug(KDECONNECT_CORE) << "Fallback (2), try reverse connection (send udp packet)"; m_udpSocket.writeDatagram(np2.serialize(), m_receivedIdentityPackets[socket].sender, UDP_PORT); } delete m_receivedIdentityPackets.take(socket).np; //We don't delete the socket because now it's owned by the LanDeviceLink } void LanLinkProvider::encrypted() { qCDebug(KDECONNECT_CORE) << "Socket successfully established an SSL connection"; QSslSocket* socket = qobject_cast(sender()); if (!socket) return; disconnect(socket, &QSslSocket::encrypted, this, &LanLinkProvider::encrypted); disconnect(socket, SIGNAL(sslErrors(QList)), this, SLOT(sslErrors(QList))); Q_ASSERT(socket->mode() != QSslSocket::UnencryptedMode); LanDeviceLink::ConnectionStarted connectionOrigin = (socket->mode() == QSslSocket::SslClientMode)? LanDeviceLink::Locally : LanDeviceLink::Remotely; NetworkPacket* receivedPacket = m_receivedIdentityPackets[socket].np; const QString& deviceId = receivedPacket->get(QStringLiteral("deviceId")); addLink(deviceId, socket, receivedPacket, connectionOrigin); // Copied from connected slot, now delete received packet delete m_receivedIdentityPackets.take(socket).np; } void LanLinkProvider::sslErrors(const QList& errors) { QSslSocket* socket = qobject_cast(sender()); if (!socket) return; disconnect(socket, &QSslSocket::encrypted, this, &LanLinkProvider::encrypted); disconnect(socket, SIGNAL(sslErrors(QList)), this, SLOT(sslErrors(QList))); qCDebug(KDECONNECT_CORE) << "Failing due to " << errors; Device* device = Daemon::instance()->getDevice(socket->peerVerifyName()); if (device) { device->unpair(); } delete m_receivedIdentityPackets.take(socket).np; // Socket disconnects itself on ssl error and will be deleted by deleteLater slot, no need to delete manually } //I'm the new device and this is the answer to my UDP identity packet (no data received yet). They are connecting to us through TCP, and they should send an identity. void LanLinkProvider::newConnection() { //qCDebug(KDECONNECT_CORE) << "LanLinkProvider newConnection"; while (m_server->hasPendingConnections()) { QSslSocket* socket = m_server->nextPendingConnection(); configureSocket(socket); //This socket is still managed by us (and child of the QTcpServer), if //it disconnects before we manage to pass it to a LanDeviceLink, it's //our responsibility to delete it. We do so with this connection. connect(socket, &QAbstractSocket::disconnected, socket, &QObject::deleteLater); connect(socket, &QIODevice::readyRead, this, &LanLinkProvider::dataReceived); } } //I'm the new device and this is the answer to my UDP identity packet (data received) void LanLinkProvider::dataReceived() { QSslSocket* socket = qobject_cast(sender()); const QByteArray data = socket->readLine(); //qCDebug(KDECONNECT_CORE) << "LanLinkProvider received reply:" << data; NetworkPacket* np = new NetworkPacket(QLatin1String("")); bool success = NetworkPacket::unserialize(data, np); if (!success) { delete np; return; } if (np->type() != PACKET_TYPE_IDENTITY) { qCWarning(KDECONNECT_CORE) << "LanLinkProvider/newConnection: Expected identity, received " << np->type(); delete np; return; } // Needed in "encrypted" if ssl is used, similar to "connected" m_receivedIdentityPackets[socket].np = np; const QString& deviceId = np->get(QStringLiteral("deviceId")); //qCDebug(KDECONNECT_CORE) << "Handshaking done (i'm the new device)"; //This socket will now be owned by the LanDeviceLink or we don't want more data to be received, forget about it disconnect(socket, &QIODevice::readyRead, this, &LanLinkProvider::dataReceived); if (np->get(QStringLiteral("protocolVersion")) >= MIN_VERSION_WITH_SSL_SUPPORT) { bool isDeviceTrusted = KdeConnectConfig::instance()->trustedDevices().contains(deviceId); configureSslSocket(socket, deviceId, isDeviceTrusted); qCDebug(KDECONNECT_CORE) << "Starting client ssl (but I'm the server TCP socket)"; connect(socket, &QSslSocket::encrypted, this, &LanLinkProvider::encrypted); if (isDeviceTrusted) { connect(socket, SIGNAL(sslErrors(QList)), this, SLOT(sslErrors(QList))); } socket->startClientEncryption(); } else { qWarning() << np->get(QStringLiteral("deviceName")) << "uses an old protocol version, this won't work"; //addLink(deviceId, socket, np, LanDeviceLink::Locally); delete m_receivedIdentityPackets.take(socket).np; } } void LanLinkProvider::deviceLinkDestroyed(QObject* destroyedDeviceLink) { const QString id = destroyedDeviceLink->property("deviceId").toString(); //qCDebug(KDECONNECT_CORE) << "deviceLinkDestroyed" << id; QMap< QString, LanDeviceLink* >::iterator linkIterator = m_links.find(id); Q_ASSERT(linkIterator != m_links.end()); if (linkIterator != m_links.end()) { Q_ASSERT(linkIterator.value() == destroyedDeviceLink); m_links.erase(linkIterator); auto pairingHandler = m_pairingHandlers.take(id); if (pairingHandler) { pairingHandler->deleteLater(); } } } void LanLinkProvider::configureSslSocket(QSslSocket* socket, const QString& deviceId, bool isDeviceTrusted) { // Setting supported ciphers manually - // Top 3 ciphers are for new Android devices, botton two are for old Android devices + // Top 3 ciphers are for new Android devices, bottom two are for old Android devices // FIXME : These cipher suites should be checked whether they are supported or not on device QList socketCiphers; socketCiphers.append(QSslCipher(QStringLiteral("ECDHE-ECDSA-AES256-GCM-SHA384"))); socketCiphers.append(QSslCipher(QStringLiteral("ECDHE-ECDSA-AES128-GCM-SHA256"))); socketCiphers.append(QSslCipher(QStringLiteral("ECDHE-RSA-AES128-SHA"))); socketCiphers.append(QSslCipher(QStringLiteral("RC4-SHA"))); socketCiphers.append(QSslCipher(QStringLiteral("RC4-MD5"))); socketCiphers.append(QSslCipher(QStringLiteral("DHE-RSA-AES256-SHA"))); // Configure for ssl QSslConfiguration sslConfig; sslConfig.setCiphers(socketCiphers); sslConfig.setProtocol(QSsl::TlsV1_0); socket->setSslConfiguration(sslConfig); socket->setLocalCertificate(KdeConnectConfig::instance()->certificate()); socket->setPrivateKey(KdeConnectConfig::instance()->privateKeyPath()); socket->setPeerVerifyName(deviceId); if (isDeviceTrusted) { QString certString = KdeConnectConfig::instance()->getDeviceProperty(deviceId, QStringLiteral("certificate"), QString()); socket->addCaCertificate(QSslCertificate(certString.toLatin1())); socket->setPeerVerifyMode(QSslSocket::VerifyPeer); } else { socket->setPeerVerifyMode(QSslSocket::QueryPeer); } //Usually SSL errors are only bad for trusted devices. Uncomment this section to log errors in any case, for debugging. //QObject::connect(socket, static_cast&)>(&QSslSocket::sslErrors), [](const QList& errors) //{ // Q_FOREACH (const QSslError& error, errors) { // qCDebug(KDECONNECT_CORE) << "SSL Error:" << error.errorString(); // } //}); } void LanLinkProvider::configureSocket(QSslSocket* socket) { socket->setProxy(QNetworkProxy::NoProxy); socket->setSocketOption(QAbstractSocket::KeepAliveOption, QVariant(1)); #ifdef TCP_KEEPIDLE // time to start sending keepalive packets (seconds) int maxIdle = 10; setsockopt(socket->socketDescriptor(), IPPROTO_TCP, TCP_KEEPIDLE, &maxIdle, sizeof(maxIdle)); #endif #ifdef TCP_KEEPINTVL // interval between keepalive packets after the initial period (seconds) int interval = 5; setsockopt(socket->socketDescriptor(), IPPROTO_TCP, TCP_KEEPINTVL, &interval, sizeof(interval)); #endif #ifdef TCP_KEEPCNT // number of missed keepalive packets before disconnecting int count = 3; setsockopt(socket->socketDescriptor(), IPPROTO_TCP, TCP_KEEPCNT, &count, sizeof(count)); #endif } void LanLinkProvider::addLink(const QString& deviceId, QSslSocket* socket, NetworkPacket* receivedPacket, LanDeviceLink::ConnectionStarted connectionOrigin) { // Socket disconnection will now be handled by LanDeviceLink disconnect(socket, &QAbstractSocket::disconnected, socket, &QObject::deleteLater); LanDeviceLink* deviceLink; //Do we have a link for this device already? QMap< QString, LanDeviceLink* >::iterator linkIterator = m_links.find(deviceId); if (linkIterator != m_links.end()) { //qCDebug(KDECONNECT_CORE) << "Reusing link to" << deviceId; deviceLink = linkIterator.value(); deviceLink->reset(socket, connectionOrigin); } else { deviceLink = new LanDeviceLink(deviceId, this, socket, connectionOrigin); connect(deviceLink, &QObject::destroyed, this, &LanLinkProvider::deviceLinkDestroyed); m_links[deviceId] = deviceLink; if (m_pairingHandlers.contains(deviceId)) { //We shouldn't have a pairinghandler if we didn't have a link. //Crash if debug, recover if release (by setting the new devicelink to the old pairinghandler) Q_ASSERT(m_pairingHandlers.contains(deviceId)); m_pairingHandlers[deviceId]->setDeviceLink(deviceLink); } } Q_EMIT onConnectionReceived(*receivedPacket, deviceLink); } LanPairingHandler* LanLinkProvider::createPairingHandler(DeviceLink* link) { LanPairingHandler* ph = m_pairingHandlers.value(link->deviceId()); if (!ph) { ph = new LanPairingHandler(link); qCDebug(KDECONNECT_CORE) << "creating pairing handler for" << link->deviceId(); connect (ph, &LanPairingHandler::pairingError, link, &DeviceLink::pairingError); m_pairingHandlers[link->deviceId()] = ph; } return ph; } void LanLinkProvider::userRequestsPair(const QString& deviceId) { LanPairingHandler* ph = createPairingHandler(m_links.value(deviceId)); ph->requestPairing(); } void LanLinkProvider::userRequestsUnpair(const QString& deviceId) { LanPairingHandler* ph = createPairingHandler(m_links.value(deviceId)); ph->unpair(); } void LanLinkProvider::incomingPairPacket(DeviceLink* deviceLink, const NetworkPacket& np) { LanPairingHandler* ph = createPairingHandler(deviceLink); ph->packetReceived(np); } diff --git a/core/backends/lan/lanlinkprovider.h b/core/backends/lan/lanlinkprovider.h index 3988e3a7..a9bbc947 100644 --- a/core/backends/lan/lanlinkprovider.h +++ b/core/backends/lan/lanlinkprovider.h @@ -1,100 +1,99 @@ /** * Copyright 2013 Albert Vaca * * 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 . */ #ifndef LANLINKPROVIDER_H #define LANLINKPROVIDER_H #include #include #include #include #include #include -#include #include "kdeconnectcore_export.h" #include "backends/linkprovider.h" #include "server.h" #include "landevicelink.h" class LanPairingHandler; class KDECONNECTCORE_EXPORT LanLinkProvider : public LinkProvider { Q_OBJECT public: LanLinkProvider(bool testMode = false); ~LanLinkProvider() override; QString name() override { return QStringLiteral("LanLinkProvider"); } int priority() override { return PRIORITY_HIGH; } void userRequestsPair(const QString& deviceId); void userRequestsUnpair(const QString& deviceId); void incomingPairPacket(DeviceLink* device, const NetworkPacket& np); static void configureSslSocket(QSslSocket* socket, const QString& deviceId, bool isDeviceTrusted); static void configureSocket(QSslSocket* socket); const static quint16 UDP_PORT = 1716; const static quint16 MIN_TCP_PORT = 1716; const static quint16 MAX_TCP_PORT = 1764; public Q_SLOTS: void onNetworkChange() override; void onStart() override; void onStop() override; void connected(); void encrypted(); void connectError(); private Q_SLOTS: void newUdpConnection(); void newConnection(); void dataReceived(); void deviceLinkDestroyed(QObject* destroyedDeviceLink); void sslErrors(const QList& errors); void broadcastToNetwork(); private: LanPairingHandler* createPairingHandler(DeviceLink* link); void onNetworkConfigurationChanged(const QNetworkConfiguration& config); void addLink(const QString& deviceId, QSslSocket* socket, NetworkPacket* receivedPacket, LanDeviceLink::ConnectionStarted connectionOrigin); Server* m_server; QUdpSocket m_udpSocket; quint16 m_tcpPort; QMap m_links; QMap m_pairingHandlers; struct PendingConnect { NetworkPacket* np; QHostAddress sender; }; QMap m_receivedIdentityPackets; QNetworkConfiguration m_lastConfig; const bool m_testMode; QTimer m_combineBroadcastsTimer; }; #endif diff --git a/core/daemon.cpp b/core/daemon.cpp index c123e47a..e5233546 100644 --- a/core/daemon.cpp +++ b/core/daemon.cpp @@ -1,307 +1,307 @@ /** * Copyright 2013 Albert Vaca * * 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 . */ #include "daemon.h" #include #include #include #include #include #include "core_debug.h" #include "kdeconnectconfig.h" #include "networkpacket.h" #ifdef KDECONNECT_BLUETOOTH #include "backends/bluetooth/bluetoothlinkprovider.h" #endif #include "backends/lan/lanlinkprovider.h" #include "backends/loopback/loopbacklinkprovider.h" #include "device.h" #include "backends/devicelink.h" #include "backends/linkprovider.h" //In older Qt released, qAsConst isnt available #include "qtcompat_p.h" static Daemon* s_instance = nullptr; struct DaemonPrivate { //Different ways to find devices and connect to them QSet m_linkProviders; //Every known device QMap m_devices; QSet m_discoveryModeAcquisitions; }; Daemon* Daemon::instance() { Q_ASSERT(s_instance != nullptr); return s_instance; } Daemon::Daemon(QObject* parent, bool testMode) : QObject(parent) , d(new DaemonPrivate) { Q_ASSERT(!s_instance); s_instance = this; qCDebug(KDECONNECT_CORE) << "KdeConnect daemon starting"; //Load backends if (testMode) d->m_linkProviders.insert(new LoopbackLinkProvider()); else { d->m_linkProviders.insert(new LanLinkProvider()); #ifdef KDECONNECT_BLUETOOTH d->m_linkProviders.insert(new BluetoothLinkProvider()); #endif } - //Read remebered paired devices + //Read remembered paired devices const QStringList& list = KdeConnectConfig::instance()->trustedDevices(); for (const QString& id : list) { addDevice(new Device(this, id)); } //Listen to new devices for (LinkProvider* a : qAsConst(d->m_linkProviders)) { connect(a, &LinkProvider::onConnectionReceived, this, &Daemon::onNewDeviceLink); a->onStart(); } //Register on DBus qDBusRegisterMetaType< QMap >(); QDBusConnection::sessionBus().registerService(QStringLiteral("org.kde.kdeconnect")); QDBusConnection::sessionBus().registerObject(QStringLiteral("/modules/kdeconnect"), this, QDBusConnection::ExportScriptableContents); qCDebug(KDECONNECT_CORE) << "KdeConnect daemon started"; } void Daemon::acquireDiscoveryMode(const QString& key) { bool oldState = d->m_discoveryModeAcquisitions.isEmpty(); d->m_discoveryModeAcquisitions.insert(key); if (oldState != d->m_discoveryModeAcquisitions.isEmpty()) { forceOnNetworkChange(); } } void Daemon::releaseDiscoveryMode(const QString& key) { bool oldState = d->m_discoveryModeAcquisitions.isEmpty(); d->m_discoveryModeAcquisitions.remove(key); if (oldState != d->m_discoveryModeAcquisitions.isEmpty()) { cleanDevices(); } } void Daemon::removeDevice(Device* device) { d->m_devices.remove(device->id()); device->deleteLater(); Q_EMIT deviceRemoved(device->id()); Q_EMIT deviceListChanged(); } void Daemon::cleanDevices() { for (Device* device : qAsConst(d->m_devices)) { if (device->isTrusted()) { continue; } device->cleanUnneededLinks(); //If there are no links remaining if (!device->isReachable()) { removeDevice(device); } } } void Daemon::forceOnNetworkChange() { qCDebug(KDECONNECT_CORE) << "Sending onNetworkChange to " << d->m_linkProviders.size() << " LinkProviders"; for (LinkProvider* a : qAsConst(d->m_linkProviders)) { a->onNetworkChange(); } } Device*Daemon::getDevice(const QString& deviceId) { for (Device* device : qAsConst(d->m_devices)) { if (device->id() == deviceId) { return device; } } return Q_NULLPTR; } QStringList Daemon::devices(bool onlyReachable, bool onlyTrusted) const { QStringList ret; for (Device* device : qAsConst(d->m_devices)) { if (onlyReachable && !device->isReachable()) continue; if (onlyTrusted && !device->isTrusted()) continue; ret.append(device->id()); } return ret; } QMap Daemon::deviceNames(bool onlyReachable, bool onlyTrusted) const { QMap ret; for (Device* device : qAsConst(d->m_devices)) { if (onlyReachable && !device->isReachable()) continue; if (onlyTrusted && !device->isTrusted()) continue; ret[device->id()] = device->name(); } return ret; } void Daemon::onNewDeviceLink(const NetworkPacket& identityPacket, DeviceLink* dl) { const QString& id = identityPacket.get(QStringLiteral("deviceId")); //qCDebug(KDECONNECT_CORE) << "Device discovered" << id << "via" << dl->provider()->name(); if (d->m_devices.contains(id)) { qCDebug(KDECONNECT_CORE) << "It is a known device" << identityPacket.get(QStringLiteral("deviceName")); Device* device = d->m_devices[id]; bool wasReachable = device->isReachable(); device->addLink(identityPacket, dl); if (!wasReachable) { Q_EMIT deviceVisibilityChanged(id, true); Q_EMIT deviceListChanged(); } } else { qCDebug(KDECONNECT_CORE) << "It is a new device" << identityPacket.get(QStringLiteral("deviceName")); Device* device = new Device(this, identityPacket, dl); //we discard the connections that we created but it's not paired. if (!isDiscoveringDevices() && !device->isTrusted() && !dl->linkShouldBeKeptAlive()) { device->deleteLater(); } else { addDevice(device); } } } void Daemon::onDeviceStatusChanged() { Device* device = (Device*)sender(); //qCDebug(KDECONNECT_CORE) << "Device" << device->name() << "status changed. Reachable:" << device->isReachable() << ". Paired: " << device->isPaired(); if (!device->isReachable() && !device->isTrusted()) { //qCDebug(KDECONNECT_CORE) << "Destroying device" << device->name(); removeDevice(device); } else { Q_EMIT deviceVisibilityChanged(device->id(), device->isReachable()); Q_EMIT deviceListChanged(); } } void Daemon::setAnnouncedName(const QString& name) { qCDebug(KDECONNECT_CORE()) << "Announcing name"; KdeConnectConfig::instance()->setName(name); forceOnNetworkChange(); Q_EMIT announcedNameChanged(name); } QString Daemon::announcedName() { return KdeConnectConfig::instance()->name(); } QNetworkAccessManager* Daemon::networkAccessManager() { static QPointer manager; if (!manager) { manager = new QNetworkAccessManager(this); } return manager; } QList Daemon::devicesList() const { return d->m_devices.values(); } bool Daemon::isDiscoveringDevices() const { return !d->m_discoveryModeAcquisitions.isEmpty(); } QString Daemon::deviceIdByName(const QString& name) const { for (Device* device : qAsConst(d->m_devices)) { if (device->name() == name && device->isTrusted()) return device->id(); } return {}; } void Daemon::addDevice(Device* device) { const QString id = device->id(); connect(device, &Device::reachableChanged, this, &Daemon::onDeviceStatusChanged); connect(device, &Device::trustedChanged, this, &Daemon::onDeviceStatusChanged); connect(device, &Device::hasPairingRequestsChanged, this, &Daemon::pairingRequestsChanged); connect(device, &Device::hasPairingRequestsChanged, this, [this, device](bool hasPairingRequests) { if (hasPairingRequests) askPairingConfirmation(device); } ); d->m_devices[id] = device; Q_EMIT deviceAdded(id); Q_EMIT deviceListChanged(); } QStringList Daemon::pairingRequests() const { QStringList ret; for(Device* dev: d->m_devices) { if (dev->hasPairingRequests()) ret += dev->id(); } return ret; } Daemon::~Daemon() { } QString Daemon::selfId() const { return KdeConnectConfig::instance()->deviceId(); } diff --git a/core/kdeconnectconfig.cpp b/core/kdeconnectconfig.cpp index 84f63c4d..dbd2ce0f 100644 --- a/core/kdeconnectconfig.cpp +++ b/core/kdeconnectconfig.cpp @@ -1,273 +1,272 @@ /** * Copyright 2015 Albert Vaca * * 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 . */ #include "kdeconnectconfig.h" #include #include #include #include #include #include #include #include #include #include #include #include -#include #include "core_debug.h" #include "dbushelper.h" #include "daemon.h" struct KdeConnectConfigPrivate { // The Initializer object sets things up, and also does cleanup when it goes out of scope // Note it's not being used anywhere. That's intended QCA::Initializer m_qcaInitializer; QCA::PrivateKey m_privateKey; QSslCertificate m_certificate; // Use QSslCertificate instead of QCA::Certificate due to compatibility with QSslSocket QSettings* m_config; QSettings* m_trustedDevices; }; KdeConnectConfig* KdeConnectConfig::instance() { static KdeConnectConfig* kcc = new KdeConnectConfig(); return kcc; } KdeConnectConfig::KdeConnectConfig() : d(new KdeConnectConfigPrivate) { //qCDebug(KDECONNECT_CORE) << "QCA supported capabilities:" << QCA::supportedFeatures().join(","); if(!QCA::isSupported("rsa")) { qCritical() << "Could not find support for RSA in your QCA installation"; Daemon::instance()->reportError( i18n("KDE Connect failed to start"), i18n("Could not find support for RSA in your QCA installation. If your " "distribution provides separate packets for QCA-ossl and QCA-gnupg, " "make sure you have them installed and try again.")); return; } //Make sure base directory exists QDir().mkpath(baseConfigDir().path()); //.config/kdeconnect/config d->m_config = new QSettings(baseConfigDir().absoluteFilePath(QStringLiteral("config")), QSettings::IniFormat); d->m_trustedDevices = new QSettings(baseConfigDir().absoluteFilePath(QStringLiteral("trusted_devices")), QSettings::IniFormat); const QFile::Permissions strict = QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser; QString keyPath = privateKeyPath(); QFile privKey(keyPath); if (privKey.exists() && privKey.open(QIODevice::ReadOnly)) { d->m_privateKey = QCA::PrivateKey::fromPEM(privKey.readAll()); } else { d->m_privateKey = QCA::KeyGenerator().createRSA(2048); if (!privKey.open(QIODevice::ReadWrite | QIODevice::Truncate)) { Daemon::instance()->reportError(QStringLiteral("KDE Connect"), i18n("Could not store private key file: %1", keyPath)); } else { privKey.setPermissions(strict); privKey.write(d->m_privateKey.toPEM().toLatin1()); } } QString certPath = certificatePath(); QFile cert(certPath); if (cert.exists() && cert.open(QIODevice::ReadOnly)) { d->m_certificate = QSslCertificate::fromPath(certPath).at(0); } else { // No certificate yet. Probably first run. Let's generate one! QString uuid = QUuid::createUuid().toString(); DbusHelper::filterNonExportableCharacters(uuid); qCDebug(KDECONNECT_CORE) << "My id:" << uuid; // FIXME: We only use QCA here to generate the cert and key, would be nice to get rid of it completely. - // The same thing we are doing with QCA could be done invoking openssl (altought it's potentially less portable): + // The same thing we are doing with QCA could be done invoking openssl (although it's potentially less portable): // openssl req -new -x509 -sha256 -newkey rsa:2048 -nodes -keyout privateKey.pem -days 3650 -out certificate.pem -subj "/O=KDE/OU=KDE Connect/CN=_e6e29ad4_2b31_4b6d_8f7a_9872dbaa9095_" QCA::CertificateOptions certificateOptions = QCA::CertificateOptions(); QDateTime startTime = QDateTime::currentDateTime().addYears(-1); QDateTime endTime = startTime.addYears(10); QCA::CertificateInfo certificateInfo; certificateInfo.insert(QCA::CommonName, uuid); certificateInfo.insert(QCA::Organization,QStringLiteral("KDE")); certificateInfo.insert(QCA::OrganizationalUnit,QStringLiteral("Kde connect")); certificateOptions.setInfo(certificateInfo); certificateOptions.setFormat(QCA::PKCS10); certificateOptions.setSerialNumber(QCA::BigInteger(10)); certificateOptions.setValidityPeriod(startTime, endTime); d->m_certificate = QSslCertificate(QCA::Certificate(certificateOptions, d->m_privateKey).toPEM().toLatin1()); if (!cert.open(QIODevice::ReadWrite | QIODevice::Truncate)) { Daemon::instance()->reportError(QStringLiteral("KDE Connect"), i18n("Could not store certificate file: %1", certPath)); } else { cert.setPermissions(strict); cert.write(d->m_certificate.toPem()); } } //Extra security check if (QFile::permissions(keyPath) != strict) { qCWarning(KDECONNECT_CORE) << "Warning: KDE Connect private key file has too open permissions " << keyPath; } } QString KdeConnectConfig::name() { QString defaultName = qgetenv("USER") + '@' + QHostInfo::localHostName(); QString name = d->m_config->value(QStringLiteral("name"), defaultName).toString(); return name; } void KdeConnectConfig::setName(const QString& name) { d->m_config->setValue(QStringLiteral("name"), name); d->m_config->sync(); } QString KdeConnectConfig::deviceType() { return QStringLiteral("desktop"); // TODO } QString KdeConnectConfig::deviceId() { return d->m_certificate.subjectInfo( QSslCertificate::CommonName ).constFirst(); } QString KdeConnectConfig::privateKeyPath() { return baseConfigDir().absoluteFilePath(QStringLiteral("privateKey.pem")); } QCA::PrivateKey KdeConnectConfig::privateKey() { return d->m_privateKey; } QCA::PublicKey KdeConnectConfig::publicKey() { return d->m_privateKey.toPublicKey(); } QString KdeConnectConfig::certificatePath() { return baseConfigDir().absoluteFilePath(QStringLiteral("certificate.pem")); } QSslCertificate KdeConnectConfig::certificate() { return d->m_certificate; } QDir KdeConnectConfig::baseConfigDir() { QString configPath = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation); QString kdeconnectConfigPath = QDir(configPath).absoluteFilePath(QStringLiteral("kdeconnect")); return QDir(kdeconnectConfigPath); } QStringList KdeConnectConfig::trustedDevices() { const QStringList& list = d->m_trustedDevices->childGroups(); return list; } void KdeConnectConfig::addTrustedDevice(const QString& id, const QString& name, const QString& type) { d->m_trustedDevices->beginGroup(id); d->m_trustedDevices->setValue(QStringLiteral("name"), name); d->m_trustedDevices->setValue(QStringLiteral("type"), type); d->m_trustedDevices->endGroup(); d->m_trustedDevices->sync(); QDir().mkpath(deviceConfigDir(id).path()); } KdeConnectConfig::DeviceInfo KdeConnectConfig::getTrustedDevice(const QString& id) { d->m_trustedDevices->beginGroup(id); KdeConnectConfig::DeviceInfo info; info.deviceName = d->m_trustedDevices->value(QStringLiteral("name"), QLatin1String("unnamed")).toString(); info.deviceType = d->m_trustedDevices->value(QStringLiteral("type"), QLatin1String("unknown")).toString(); d->m_trustedDevices->endGroup(); return info; } void KdeConnectConfig::removeTrustedDevice(const QString& deviceId) { d->m_trustedDevices->remove(deviceId); d->m_trustedDevices->sync(); //We do not remove the config files. } // Utility functions to set and get a value void KdeConnectConfig::setDeviceProperty(const QString& deviceId, const QString& key, const QString& value) { d->m_trustedDevices->beginGroup(deviceId); d->m_trustedDevices->setValue(key, value); d->m_trustedDevices->endGroup(); d->m_trustedDevices->sync(); } QString KdeConnectConfig::getDeviceProperty(const QString& deviceId, const QString& key, const QString& defaultValue) { QString value; d->m_trustedDevices->beginGroup(deviceId); value = d->m_trustedDevices->value(key, defaultValue).toString(); d->m_trustedDevices->endGroup(); return value; } QDir KdeConnectConfig::deviceConfigDir(const QString& deviceId) { QString deviceConfigPath = baseConfigDir().absoluteFilePath(deviceId); return QDir(deviceConfigPath); } QDir KdeConnectConfig::pluginConfigDir(const QString& deviceId, const QString& pluginName) { QString deviceConfigPath = baseConfigDir().absoluteFilePath(deviceId); QString pluginConfigDir = QDir(deviceConfigPath).absoluteFilePath(pluginName); return QDir(pluginConfigDir); } diff --git a/declarativeplugin/kdeconnectdeclarativeplugin.cpp b/declarativeplugin/kdeconnectdeclarativeplugin.cpp index d86c0b11..d42425cf 100644 --- a/declarativeplugin/kdeconnectdeclarativeplugin.cpp +++ b/declarativeplugin/kdeconnectdeclarativeplugin.cpp @@ -1,152 +1,152 @@ /** * Copyright 2013 Albert Vaca * * 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 . */ #include "kdeconnectdeclarativeplugin.h" #include #include #include #include #include #include "objectfactory.h" #include "responsewaiter.h" #include "interfaces/devicessortproxymodel.h" #include "interfaces/devicesmodel.h" #include "interfaces/notificationsmodel.h" #include QObject* createDeviceDbusInterface(const QVariant& deviceId) { return new DeviceDbusInterface(deviceId.toString()); } QObject* createDeviceBatteryDbusInterface(const QVariant& deviceId) { return new DeviceBatteryDbusInterface(deviceId.toString()); } 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()); } QObject* createRemoteControlInterface(const QVariant& deviceId) { return new RemoteControlDbusInterface(deviceId.toString()); } QObject* createMprisInterface(const QVariant& deviceId) { return new MprisDbusInterface(deviceId.toString()); } QObject* createDeviceLockInterface(const QVariant& deviceId) { Q_ASSERT(!deviceId.toString().isEmpty()); return new LockDeviceDbusInterface(deviceId.toString()); } QObject* createSmsInterface(const QVariant& deviceId) { return new SmsDbusInterface(deviceId.toString()); } QObject* createDBusResponse() { return new DBusAsyncResponse(); } QObject* createRemoteCommandsInterface(const QVariant& deviceId) { return new RemoteCommandsDbusInterface(deviceId.toString()); } void KdeConnectDeclarativePlugin::registerTypes(const char* uri) { qmlRegisterType(uri, 1, 0, "DevicesModel"); qmlRegisterType(uri, 1, 0, "NotificationsModel"); qmlRegisterType(uri, 1, 0, "RemoteCommandsModel"); qmlRegisterType(uri, 1, 0, "DBusAsyncResponse"); qmlRegisterType(uri, 1, 0, "DevicesSortProxyModel"); - qmlRegisterUncreatableType(uri, 1, 0, "MprisDbusInterface", QStringLiteral("You're not supposed to instantiate interfacess")); - qmlRegisterUncreatableType(uri, 1, 0, "LockDeviceDbusInterface", QStringLiteral("You're not supposed to instantiate interfacess")); - qmlRegisterUncreatableType(uri, 1, 0, "FindMyPhoneDbusInterface", QStringLiteral("You're not supposed to instantiate interfacess")); - qmlRegisterUncreatableType(uri, 1, 0, "RemoteKeyboardDbusInterface", QStringLiteral("You're not supposed to instantiate interfacess")); - qmlRegisterUncreatableType(uri, 1, 0, "DeviceDbusInterface", QStringLiteral("You're not supposed to instantiate interfacess")); - qmlRegisterUncreatableType(uri, 1, 0, "RemoteCommandsDbusInterface", QStringLiteral("You're not supposed to instantiate interfacess")); + qmlRegisterUncreatableType(uri, 1, 0, "MprisDbusInterface", QStringLiteral("You're not supposed to instantiate interfaces")); + qmlRegisterUncreatableType(uri, 1, 0, "LockDeviceDbusInterface", QStringLiteral("You're not supposed to instantiate interfaces")); + qmlRegisterUncreatableType(uri, 1, 0, "FindMyPhoneDbusInterface", QStringLiteral("You're not supposed to instantiate interfaces")); + qmlRegisterUncreatableType(uri, 1, 0, "RemoteKeyboardDbusInterface", QStringLiteral("You're not supposed to instantiate interfaces")); + qmlRegisterUncreatableType(uri, 1, 0, "DeviceDbusInterface", QStringLiteral("You're not supposed to instantiate interfaces")); + qmlRegisterUncreatableType(uri, 1, 0, "RemoteCommandsDbusInterface", QStringLiteral("You're not supposed to instantiate interfaces")); qmlRegisterSingletonType(uri, 1, 0, "DaemonDbusInterface", [](QQmlEngine*, QJSEngine*) -> QObject* { return new DaemonDbusInterface; } ); } void KdeConnectDeclarativePlugin::initializeEngine(QQmlEngine* engine, const char* uri) { QQmlExtensionPlugin::initializeEngine(engine, uri); engine->rootContext()->setContextProperty(QStringLiteral("DeviceDbusInterfaceFactory") , new ObjectFactory(engine, createDeviceDbusInterface)); engine->rootContext()->setContextProperty(QStringLiteral("DeviceBatteryDbusInterfaceFactory") , new ObjectFactory(engine, createDeviceBatteryDbusInterface)); engine->rootContext()->setContextProperty(QStringLiteral("FindMyPhoneDbusInterfaceFactory") , new ObjectFactory(engine, createFindMyPhoneInterface)); 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)); engine->rootContext()->setContextProperty(QStringLiteral("RemoteControlDbusInterfaceFactory") , new ObjectFactory(engine, createRemoteControlInterface)); engine->rootContext()->setContextProperty(QStringLiteral("LockDeviceDbusInterfaceFactory") , new ObjectFactory(engine, createDeviceLockInterface)); engine->rootContext()->setContextProperty(QStringLiteral("SmsDbusInterfaceFactory") , new ObjectFactory(engine, createSmsInterface)); engine->rootContext()->setContextProperty(QStringLiteral("DBusResponseFactory") , new ObjectFactory(engine, createDBusResponse)); engine->rootContext()->setContextProperty(QStringLiteral("DBusResponseWaiter") , DBusResponseWaiter::instance()); engine->rootContext()->setContextProperty(QStringLiteral("RemoteCommandsDbusInterfaceFactory") , new ObjectFactory(engine, createRemoteCommandsInterface)); } diff --git a/declarativeplugin/responsewaiter.h b/declarativeplugin/responsewaiter.h index 35c8abc8..92c9873e 100644 --- a/declarativeplugin/responsewaiter.h +++ b/declarativeplugin/responsewaiter.h @@ -1,61 +1,61 @@ #ifndef RESPONSE_WAITER_H #define RESPONSE_WAITER_H #include #include #include class QDBusPendingCall; class QDBusPendingCallWatcher; class DBusResponseWaiter : public QObject { Q_OBJECT public: static DBusResponseWaiter* instance(); - ///extract QDbusPendingCall from \p variant and blocks untill completed + ///extract QDbusPendingCall from \p variant and blocks until completed Q_INVOKABLE QVariant waitForReply(QVariant variant) const; const QDBusPendingCall* extractPendingCall(QVariant& variant) const; private: DBusResponseWaiter(); static DBusResponseWaiter* m_instance; QList m_registered; }; class DBusAsyncResponse : public QObject { Q_OBJECT Q_PROPERTY(bool autoDelete READ autodelete WRITE setAutodelete) public: explicit DBusAsyncResponse(QObject* parent = nullptr); ~DBusAsyncResponse() override = default; Q_INVOKABLE void setPendingCall(QVariant e); void setAutodelete(bool b) {m_autodelete = b;}; bool autodelete() const {return m_autodelete;} Q_SIGNALS: void success(const QVariant& result); void error(const QString& message); private Q_SLOTS: void onCallFinished(QDBusPendingCallWatcher* watcher); void onTimeout(); private: QTimer m_timeout; bool m_autodelete; }; #endif diff --git a/interfaces/modeltest.cpp b/interfaces/modeltest.cpp index c3c86aeb..9fb6d998 100644 --- a/interfaces/modeltest.cpp +++ b/interfaces/modeltest.cpp @@ -1,539 +1,539 @@ /**************************************************************************** ** ** Copyright (C) 2007 Trolltech ASA. All rights reserved. ** ** This file is part of the Qt Concurrent project on Trolltech Labs. ** ** This file may be used under the terms of the GNU General Public ** License version 2.0 as published by the Free Software Foundation ** and appearing in the file LICENSE.GPL included in the packaging of ** this file. Please review the following information to ensure GNU ** General Public Licensing requirements will be met: ** http://www.trolltech.com/products/qt/opensource.html ** ** If you are unsure which license is appropriate for your use, please ** review the following information: ** http://www.trolltech.com/products/qt/licensing.html or contact the ** sales department at sales@trolltech.com. ** ** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE ** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. ** ****************************************************************************/ #include "modeltest.h" -#include +#include Q_DECLARE_METATYPE(QModelIndex) /*! Connect to all of the models signals. Whenever anything happens recheck everything. */ ModelTest::ModelTest(QAbstractItemModel *_model, QObject* parent) : QObject(parent), model(_model), fetchingMore(false) { Q_ASSERT(model); connect(model, SIGNAL(columnsAboutToBeInserted(QModelIndex,int,int)), this, SLOT(runAllTests())); connect(model, SIGNAL(columnsAboutToBeRemoved(QModelIndex,int,int)), this, SLOT(runAllTests())); connect(model, SIGNAL(columnsInserted(QModelIndex,int,int)), this, SLOT(runAllTests())); connect(model, SIGNAL(columnsRemoved(QModelIndex,int,int)), this, SLOT(runAllTests())); connect(model, SIGNAL(dataChanged(QModelIndex,QModelIndex)), this, SLOT(runAllTests())); connect(model, SIGNAL(headerDataChanged(Qt::Orientation,int,int)), this, SLOT(runAllTests())); connect(model, SIGNAL(layoutAboutToBeChanged()), this, SLOT(runAllTests())); connect(model, SIGNAL(layoutChanged()), this, SLOT(runAllTests())); connect(model, SIGNAL(modelReset()), this, SLOT(runAllTests())); connect(model, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)), this, SLOT(runAllTests())); connect(model, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), this, SLOT(runAllTests())); connect(model, SIGNAL(rowsInserted(QModelIndex,int,int)), this, SLOT(runAllTests())); connect(model, SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(runAllTests())); // Special checks for inserting/removing connect(model, SIGNAL(layoutAboutToBeChanged()), this, SLOT(layoutAboutToBeChanged())); connect(model, SIGNAL(layoutChanged()), this, SLOT(layoutChanged())); connect(model, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)), this, SLOT(rowsAboutToBeInserted(QModelIndex,int,int))); connect(model, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), this, SLOT(rowsAboutToBeRemoved(QModelIndex,int,int))); connect(model, SIGNAL(rowsInserted(QModelIndex,int,int)), this, SLOT(rowsInserted(QModelIndex,int,int))); connect(model, SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(rowsRemoved(QModelIndex,int,int))); runAllTests(); } void ModelTest::runAllTests() { if (fetchingMore) return; nonDestructiveBasicTest(); rowCount(); columnCount(); hasIndex(); index(); parent(); data(); } /*! nonDestructiveBasicTest tries to call a number of the basic functions (not all) to make sure the model doesn't outright segfault, testing the functions that makes sense. */ void ModelTest::nonDestructiveBasicTest() { Q_ASSERT(model->buddy(QModelIndex()) == QModelIndex()); model->canFetchMore(QModelIndex()); Q_ASSERT(model->columnCount(QModelIndex()) >= 0); Q_ASSERT(model->data(QModelIndex()) == QVariant()); fetchingMore = true; model->fetchMore(QModelIndex()); fetchingMore = false; Qt::ItemFlags flags = model->flags(QModelIndex()); Q_ASSERT(flags == Qt::ItemIsDropEnabled || flags == 0); model->hasChildren(QModelIndex()); model->hasIndex(0, 0); model->headerData(0, Qt::Horizontal); model->index(0, 0); Q_ASSERT(model->index(-1, -1) == QModelIndex()); model->itemData(QModelIndex()); QVariant cache; model->match(QModelIndex(), -1, cache); model->mimeTypes(); Q_ASSERT(model->parent(QModelIndex()) == QModelIndex()); Q_ASSERT(model->rowCount() >= 0); QVariant variant; model->setData(QModelIndex(), variant, -1); model->setHeaderData(-1, Qt::Horizontal, QVariant()); model->setHeaderData(0, Qt::Horizontal, QVariant()); model->setHeaderData(999999, Qt::Horizontal, QVariant()); QMap roles; model->sibling(0, 0, QModelIndex()); model->span(QModelIndex()); model->supportedDropActions(); } /*! Tests model's implementation of QAbstractItemModel::rowCount() and hasChildren() Models that are dynamically populated are not as fully tested here. */ void ModelTest::rowCount() { // check top row QModelIndex topIndex = model->index(0, 0, QModelIndex()); int rows = model->rowCount(topIndex); Q_ASSERT(rows >= 0); if (rows > 0) Q_ASSERT(model->hasChildren(topIndex) == true); QModelIndex secondLevelIndex = model->index(0, 0, topIndex); if (secondLevelIndex.isValid()) { // not the top level // check a row count where parent is valid rows = model->rowCount(secondLevelIndex); Q_ASSERT(rows >= 0); if (rows > 0) Q_ASSERT(model->hasChildren(secondLevelIndex) == true); } // The models rowCount() is tested more extensively in checkChildren(), // but this catches the big mistakes } /*! Tests model's implementation of QAbstractItemModel::columnCount() and hasChildren() */ void ModelTest::columnCount() { // check top row QModelIndex topIndex = model->index(0, 0, QModelIndex()); Q_ASSERT(model->columnCount(topIndex) >= 0); // check a column count where parent is valid QModelIndex childIndex = model->index(0, 0, topIndex); if (childIndex.isValid()) Q_ASSERT(model->columnCount(childIndex) >= 0); // columnCount() is tested more extensively in checkChildren(), // but this catches the big mistakes } /*! Tests model's implementation of QAbstractItemModel::hasIndex() */ void ModelTest::hasIndex() { // Make sure that invalid values returns an invalid index Q_ASSERT(model->hasIndex(-2, -2) == false); Q_ASSERT(model->hasIndex(-2, 0) == false); Q_ASSERT(model->hasIndex(0, -2) == false); int rows = model->rowCount(); int columns = model->columnCount(); // check out of bounds Q_ASSERT(model->hasIndex(rows, columns) == false); Q_ASSERT(model->hasIndex(rows + 1, columns + 1) == false); if (rows > 0) Q_ASSERT(model->hasIndex(0, 0) == true); // hasIndex() is tested more extensively in checkChildren(), // but this catches the big mistakes } /*! Tests model's implementation of QAbstractItemModel::index() */ void ModelTest::index() { // Make sure that invalid values returns an invalid index Q_ASSERT(model->index(-2, -2) == QModelIndex()); Q_ASSERT(model->index(-2, 0) == QModelIndex()); Q_ASSERT(model->index(0, -2) == QModelIndex()); int rows = model->rowCount(); int columns = model->columnCount(); if (rows == 0) return; // Catch off by one errors Q_ASSERT(model->index(rows, columns) == QModelIndex()); Q_ASSERT(model->index(0, 0).isValid() == true); // Make sure that the same index is* always* returned QModelIndex a = model->index(0, 0); QModelIndex b = model->index(0, 0); Q_ASSERT(a == b); // index() is tested more extensively in checkChildren(), // but this catches the big mistakes } /*! Tests model's implementation of QAbstractItemModel::parent() */ void ModelTest::parent() { // Make sure the model wont crash and will return an invalid QModelIndex // when asked for the parent of an invalid index. Q_ASSERT(model->parent(QModelIndex()) == QModelIndex()); if (model->rowCount() == 0) return; // Column 0 | Column 1 | // QModelIndex() | | // \- topIndex | topIndex1 | // \- childIndex | childIndex1 | // Common error test #1, make sure that a top level index has a parent // that is a invalid QModelIndex. QModelIndex topIndex = model->index(0, 0, QModelIndex()); Q_ASSERT(model->parent(topIndex) == QModelIndex()); // Common error test #2, make sure that a second level index has a parent // that is the first level index. if (model->rowCount(topIndex) > 0) { QModelIndex childIndex = model->index(0, 0, topIndex); Q_ASSERT(model->parent(childIndex) == topIndex); } // Common error test #3, the second column should NOT have the same children // as the first column in a row. // Usually the second column shouldn't have children. QModelIndex topIndex1 = model->index(0, 1, QModelIndex()); if (model->rowCount(topIndex1) > 0) { QModelIndex childIndex = model->index(0, 0, topIndex); QModelIndex childIndex1 = model->index(0, 0, topIndex1); Q_ASSERT(childIndex != childIndex1); } // Full test, walk n levels deep through the model making sure that all // parent's children correctly specify their parent. checkChildren(QModelIndex()); } /*! Called from the parent() test. A model that returns an index of parent X should also return X when asking for the parent of the index. This recursive function does pretty extensive testing on the whole model in an effort to catch edge cases. This function assumes that rowCount(), columnCount() and index() already work. If they have a bug it will point it out, but the above tests should have already found the basic bugs because it is easier to figure out the problem in those tests then this one. */ void ModelTest::checkChildren(const QModelIndex &parent, int currentDepth) { // First just try walking back up the tree. QModelIndex p = parent; while (p.isValid()) p = p.parent(); // For models that are dynamically populated if (model->canFetchMore(parent)) { fetchingMore = true; model->fetchMore(parent); fetchingMore = false; } int rows = model->rowCount(parent); int columns = model->columnCount(parent); if (rows > 0) Q_ASSERT(model->hasChildren(parent)); // Some further testing against rows(), columns(), and hasChildren() Q_ASSERT(rows >= 0); Q_ASSERT(columns >= 0); if (rows > 0) Q_ASSERT(model->hasChildren(parent) == true); //qDebug() << "parent:" << model->data(parent).toString() << "rows:" << rows // << "columns:" << columns << "parent column:" << parent.column(); Q_ASSERT(model->hasIndex(rows + 1, 0, parent) == false); for (int r = 0; r < rows; ++r) { if (model->canFetchMore(parent)) { fetchingMore = true; model->fetchMore(parent); fetchingMore = false; } Q_ASSERT(model->hasIndex(r, columns + 1, parent) == false); for (int c = 0; c < columns; ++c) { Q_ASSERT(model->hasIndex(r, c, parent) == true); QModelIndex index = model->index(r, c, parent); // rowCount() and columnCount() said that it existed... Q_ASSERT(index.isValid() == true); // index() should always return the same index when called twice in a row QModelIndex modifiedIndex = model->index(r, c, parent); Q_ASSERT(index == modifiedIndex); // Make sure we get the same index if we request it twice in a row QModelIndex a = model->index(r, c, parent); QModelIndex b = model->index(r, c, parent); Q_ASSERT(a == b); // Some basic checking on the index that is returned Q_ASSERT(index.model() == model); Q_ASSERT(index.row() == r); Q_ASSERT(index.column() == c); // While you can technically return a QVariant usually this is a sign // of an bug in data() Disable if this really is ok in your model. //Q_ASSERT(model->data(index, Qt::DisplayRole).isValid() == true); // If the next test fails here is some somewhat useful debug you play with. /* if (model->parent(index) != parent) { qDebug() << r << c << currentDepth << model->data(index).toString() << model->data(parent).toString(); qDebug() << index << parent << model->parent(index); // And a view that you can even use to show the model. //QTreeView view; //view.setModel(model); //view.show(); }*/ // Check that we can get back our real parent. QModelIndex p = model->parent(index); //qDebug() << "child:" << index; //qDebug() << p; //qDebug() << parent; Q_ASSERT(p == parent); // recursively go down the children if (model->hasChildren(index) && currentDepth < 10 ) { //qDebug() << r << c << "has children" << model->rowCount(index); checkChildren(index, ++currentDepth); }/* else { if (currentDepth >= 10) qDebug() << "checked 10 deep"; };*/ // make sure that after testing the children that the index doesn't change. QModelIndex newerIndex = model->index(r, c, parent); Q_ASSERT(index == newerIndex); } } } /*! Tests model's implementation of QAbstractItemModel::data() */ void ModelTest::data() { // Invalid index should return an invalid qvariant Q_ASSERT(!model->data(QModelIndex()).isValid()); if (model->rowCount() == 0) return; // A valid index should have a valid QVariant data Q_ASSERT(model->index(0, 0).isValid()); // shouldn't be able to set data on an invalid index Q_ASSERT(model->setData(QModelIndex(), QLatin1String("foo"), Qt::DisplayRole) == false); // General Purpose roles that should return a QString QVariant variant = model->data(model->index(0, 0), Qt::ToolTipRole); if (variant.isValid()) { Q_ASSERT(variant.canConvert()); } variant = model->data(model->index(0, 0), Qt::StatusTipRole); if (variant.isValid()) { Q_ASSERT(variant.canConvert()); } variant = model->data(model->index(0, 0), Qt::WhatsThisRole); if (variant.isValid()) { Q_ASSERT(variant.canConvert()); } // General Purpose roles that should return a QSize variant = model->data(model->index(0, 0), Qt::SizeHintRole); if (variant.isValid()) { Q_ASSERT(variant.canConvert()); } // General Purpose roles that should return a QFont QVariant fontVariant = model->data(model->index(0, 0), Qt::FontRole); if (fontVariant.isValid()) { Q_ASSERT(fontVariant.canConvert()); } // Check that the alignment is one we know about QVariant textAlignmentVariant = model->data(model->index(0, 0), Qt::TextAlignmentRole); if (textAlignmentVariant.isValid()) { int alignment = textAlignmentVariant.toInt(); Q_ASSERT(alignment == Qt::AlignLeft || alignment == Qt::AlignRight || alignment == Qt::AlignHCenter || alignment == Qt::AlignJustify || alignment == Qt::AlignTop || alignment == Qt::AlignBottom || alignment == Qt::AlignVCenter || alignment == Qt::AlignCenter || alignment == Qt::AlignAbsolute || alignment == Qt::AlignLeading || alignment == Qt::AlignTrailing); } // General Purpose roles that should return a QColor QVariant colorVariant = model->data(model->index(0, 0), Qt::BackgroundColorRole); if (colorVariant.isValid()) { Q_ASSERT(colorVariant.canConvert()); } colorVariant = model->data(model->index(0, 0), Qt::TextColorRole); if (colorVariant.isValid()) { Q_ASSERT(colorVariant.canConvert()); } // Check that the "check state" is one we know about. QVariant checkStateVariant = model->data(model->index(0, 0), Qt::CheckStateRole); if (checkStateVariant.isValid()) { int state = checkStateVariant.toInt(); Q_ASSERT(state == Qt::Unchecked || state == Qt::PartiallyChecked || state == Qt::Checked); } } /*! Store what is about to be inserted to make sure it actually happens \sa rowsInserted() */ void ModelTest::rowsAboutToBeInserted(const QModelIndex &parent, int start, int end) { Q_UNUSED(end); Changing c; c.parent = parent; c.oldSize = model->rowCount(parent); c.last = model->data(model->index(start - 1, 0, parent)); c.next = model->data(model->index(start, 0, parent)); insert.push(c); } /*! Confirm that what was said was going to happen actually did \sa rowsAboutToBeInserted() */ void ModelTest::rowsInserted(const QModelIndex & parent, int start, int end) { Changing c = insert.pop(); Q_ASSERT(c.parent == parent); Q_ASSERT(c.oldSize + (end - start + 1) == model->rowCount(parent)); Q_ASSERT(c.last == model->data(model->index(start - 1, 0, c.parent))); /* if (c.next != model->data(model->index(end + 1, 0, c.parent))) { qDebug() << start << end; for (int i=0; i < model->rowCount(); ++i) qDebug() << model->index(i, 0).data().toString(); qDebug() << c.next << model->data(model->index(end + 1, 0, c.parent)); } */ Q_ASSERT(c.next == model->data(model->index(end + 1, 0, c.parent))); } void ModelTest::layoutAboutToBeChanged() { for (int i = 0; i < qBound(0, model->rowCount(), 100); ++i) changing.append(QPersistentModelIndex(model->index(i, 0))); } void ModelTest::layoutChanged() { for (int i = 0; i < changing.count(); ++i) { QPersistentModelIndex p = changing[i]; Q_ASSERT(p == model->index(p.row(), p.column(), p.parent())); } changing.clear(); } /*! Store what is about to be inserted to make sure it actually happens \sa rowsRemoved() */ void ModelTest::rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end) { Changing c; c.parent = parent; c.oldSize = model->rowCount(parent); c.last = model->data(model->index(start - 1, 0, parent)); c.next = model->data(model->index(end + 1, 0, parent)); remove.push(c); } /*! Confirm that what was said was going to happen actually did \sa rowsAboutToBeRemoved() */ void ModelTest::rowsRemoved(const QModelIndex & parent, int start, int end) { Changing c = remove.pop(); Q_ASSERT(c.parent == parent); Q_ASSERT(c.oldSize - (end - start + 1) == model->rowCount(parent)); Q_ASSERT(c.last == model->data(model->index(start - 1, 0, c.parent))); Q_ASSERT(c.next == model->data(model->index(start, 0, c.parent))); } diff --git a/interfaces/modeltest.h b/interfaces/modeltest.h index befe270e..30e39b43 100644 --- a/interfaces/modeltest.h +++ b/interfaces/modeltest.h @@ -1,76 +1,76 @@ /**************************************************************************** ** ** Copyright (C) 2007 Trolltech ASA. All rights reserved. ** ** This file is part of the Qt Concurrent project on Trolltech Labs. ** ** This file may be used under the terms of the GNU General Public ** License version 2.0 as published by the Free Software Foundation ** and appearing in the file LICENSE.GPL included in the packaging of ** this file. Please review the following information to ensure GNU ** General Public Licensing requirements will be met: ** http://www.trolltech.com/products/qt/opensource.html ** ** If you are unsure which license is appropriate for your use, please ** review the following information: ** http://www.trolltech.com/products/qt/licensing.html or contact the ** sales department at sales@trolltech.com. ** ** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE ** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. ** ****************************************************************************/ #ifndef MODELTEST_H #define MODELTEST_H -#include -#include -#include +#include +#include +#include class ModelTest : public QObject { Q_OBJECT public: explicit ModelTest(QAbstractItemModel* model, QObject* parent = 0); private Q_SLOTS: void nonDestructiveBasicTest(); void rowCount(); void columnCount(); void hasIndex(); void index(); void parent(); void data(); protected Q_SLOTS: void runAllTests(); void layoutAboutToBeChanged(); void layoutChanged(); void rowsAboutToBeInserted(const QModelIndex& parent, int start, int end); void rowsInserted(const QModelIndex & parent, int start, int end); void rowsAboutToBeRemoved(const QModelIndex& parent, int start, int end); void rowsRemoved(const QModelIndex & parent, int start, int end); private: void checkChildren(const QModelIndex& parent, int currentDepth = 0); QAbstractItemModel* model; struct Changing { QModelIndex parent; int oldSize; QVariant last; QVariant next; }; QStack insert; QStack remove; bool fetchingMore; QList changing; }; #endif diff --git a/kio/kiokdeconnect.cpp b/kio/kiokdeconnect.cpp index 02243d09..e0d7973d 100644 --- a/kio/kiokdeconnect.cpp +++ b/kio/kiokdeconnect.cpp @@ -1,234 +1,234 @@ /** * Copyright 2013 Albert Vaca * * 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 . */ #include "kiokdeconnect.h" -#include +#include #include #include #include Q_LOGGING_CATEGORY(KDECONNECT_KIO, "kdeconnect.kio") extern "C" int Q_DECL_EXPORT kdemain(int argc, char** argv) { if (argc != 4) { fprintf(stderr, "Usage: kio_kdeconnect protocol pool app\n"); exit(-1); } KioKdeconnect slave(argv[2], argv[3]); slave.dispatchLoop(); return 0; } //Some useful error mapping KIO::Error toKioError(const QDBusError::ErrorType type) { switch (type) { case QDBusError::NoError: return KIO::Error(KJob::NoError); case QDBusError::NoMemory: return KIO::ERR_OUT_OF_MEMORY; case QDBusError::Timeout: return KIO::ERR_SERVER_TIMEOUT; case QDBusError::TimedOut: return KIO::ERR_SERVER_TIMEOUT; default: return KIO::ERR_INTERNAL; }; }; template bool handleDBusError(QDBusReply& reply, KIO::SlaveBase* slave) { if (!reply.isValid()) { qCDebug(KDECONNECT_KIO) << "Error in DBus request:" << reply.error(); slave->error(toKioError(reply.error().type()),reply.error().message()); return true; } return false; } KioKdeconnect::KioKdeconnect(const QByteArray& pool, const QByteArray& app) : SlaveBase("kdeconnect", pool, app), m_dbusInterface(new DaemonDbusInterface(this)) { } void KioKdeconnect::listAllDevices() { infoMessage(i18n("Listing devices...")); //TODO: Change to all devices and show different icons for connected and disconnected? const QStringList devices = m_dbusInterface->devices(true, true); for (const QString& deviceId : devices) { DeviceDbusInterface interface(deviceId); if (!interface.hasPlugin(QStringLiteral("kdeconnect_sftp"))) continue; const QString path = QStringLiteral("kdeconnect://").append(deviceId).append("/"); const QString name = interface.name(); const QString icon = QStringLiteral("kdeconnect"); KIO::UDSEntry entry; entry.insert(KIO::UDSEntry::UDS_NAME, name); entry.insert(KIO::UDSEntry::UDS_ICON_NAME, icon); entry.insert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR); entry.insert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH); entry.insert(KIO::UDSEntry::UDS_MIME_TYPE, QLatin1String("")); entry.insert(KIO::UDSEntry::UDS_URL, path); listEntry(entry); } // We also need a non-null and writable UDSentry for "." KIO::UDSEntry entry; entry.insert(KIO::UDSEntry::UDS_NAME, QStringLiteral(".")); entry.insert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR); entry.insert(KIO::UDSEntry::UDS_SIZE, 0); entry.insert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IWGRP | S_IXGRP | S_IROTH | S_IXOTH); listEntry(entry); infoMessage(QLatin1String("")); finished(); } void KioKdeconnect::listDevice() { infoMessage(i18n("Accessing device...")); qCDebug(KDECONNECT_KIO) << "ListDevice" << m_currentDevice; SftpDbusInterface interface(m_currentDevice); QDBusReply mountreply = interface.mountAndWait(); if (handleDBusError(mountreply, this)) { return; } if (!mountreply.value()) { error(KIO::ERR_COULD_NOT_MOUNT, i18n("Could not mount device filesystem")); return; } QDBusReply< QVariantMap > urlreply = interface.getDirectories(); if (handleDBusError(urlreply, this)) { return; } QVariantMap urls = urlreply.value(); for (QVariantMap::iterator it = urls.begin(); it != urls.end(); ++it) { const QString path = it.key(); const QString name = it.value().toString(); const QString icon = QStringLiteral("folder"); KIO::UDSEntry entry; entry.insert(KIO::UDSEntry::UDS_NAME, QStringLiteral("files")); entry.insert(KIO::UDSEntry::UDS_DISPLAY_NAME, name); entry.insert(KIO::UDSEntry::UDS_ICON_NAME, icon); entry.insert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR); entry.insert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH); entry.insert(KIO::UDSEntry::UDS_MIME_TYPE, QLatin1String("")); entry.insert(KIO::UDSEntry::UDS_URL, QUrl::fromLocalFile(path).toString()); listEntry(entry); } // We also need a non-null and writable UDSentry for "." KIO::UDSEntry entry; entry.insert(KIO::UDSEntry::UDS_NAME, QStringLiteral(".")); entry.insert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR); entry.insert(KIO::UDSEntry::UDS_SIZE, 0); entry.insert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IWGRP | S_IXGRP | S_IROTH | S_IXOTH); listEntry(entry); infoMessage(QLatin1String("")); finished(); } void KioKdeconnect::listDir(const QUrl& url) { qCDebug(KDECONNECT_KIO) << "Listing..." << url; /// Url is not used here because all we could care about the url is the host, and that's already /// handled in @p setHost Q_UNUSED(url); if (!m_dbusInterface->isValid()) { infoMessage(i18n("Could not contact background service.")); finished(); return; } if (m_currentDevice.isEmpty()) { listAllDevices(); } else { listDevice(); } } void KioKdeconnect::stat(const QUrl& url) { qCDebug(KDECONNECT_KIO) << "Stat: " << url; KIO::UDSEntry entry; entry.insert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR); statEntry(entry); finished(); } void KioKdeconnect::get(const QUrl& url) { qCDebug(KDECONNECT_KIO) << "Get: " << url; mimeType(QLatin1String("")); finished(); } void KioKdeconnect::setHost(const QString& hostName, quint16 port, const QString& user, const QString& pass) { //This is called before everything else to set the file we want to show qCDebug(KDECONNECT_KIO) << "Setting host: " << hostName; // In this kio only the hostname is used Q_UNUSED(port) Q_UNUSED(user) Q_UNUSED(pass) m_currentDevice = hostName; } diff --git a/plasmoid/package/contents/ui/RemoteKeyboard.qml b/plasmoid/package/contents/ui/RemoteKeyboard.qml index b82f29f2..e3a92546 100644 --- a/plasmoid/package/contents/ui/RemoteKeyboard.qml +++ b/plasmoid/package/contents/ui/RemoteKeyboard.qml @@ -1,73 +1,73 @@ /** * Copyright 2017 Holger Kaelberer * * 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 . */ 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 + // set, but no visible '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 } } } diff --git a/plugins/contacts/CMakeLists.txt b/plugins/contacts/CMakeLists.txt index 9e3bbe47..a94244fc 100644 --- a/plugins/contacts/CMakeLists.txt +++ b/plugins/contacts/CMakeLists.txt @@ -1,11 +1,11 @@ set(kdeconnect_contacts_SRCS contactsplugin.cpp ) kdeconnect_add_plugin(kdeconnect_contacts JSON kdeconnect_contacts.json SOURCES ${kdeconnect_contacts_SRCS}) target_link_libraries(kdeconnect_contacts kdeconnectcore Qt5::DBus KF5::I18n -) \ No newline at end of file +) diff --git a/plugins/notifications/notification.cpp b/plugins/notifications/notification.cpp index 97d1cb89..fcd73cd3 100644 --- a/plugins/notifications/notification.cpp +++ b/plugins/notifications/notification.cpp @@ -1,191 +1,191 @@ /** * Copyright 2013 Albert Vaca * * 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 . */ #include "notification.h" #include "notification_debug.h" #include #include #include #include #include #include #include #include QMap Notification::s_downloadsInProgress; Notification::Notification(const NetworkPacket& np, QObject* parent) : QObject(parent) { //Make a own directory for each user so noone can see each others icons QString username; #ifdef Q_OS_WIN username = qgetenv("USERNAME"); #else username = qgetenv("USER"); #endif m_imagesDir = QDir::temp().absoluteFilePath(QStringLiteral("kdeconnect_") + username); m_imagesDir.mkpath(m_imagesDir.absolutePath()); QFile(m_imagesDir.absolutePath()).setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner | QFileDevice::ExeOwner); m_closed = false; m_ready = false; parseNetworkPacket(np); createKNotification(false, np); } Notification::~Notification() { } void Notification::dismiss() { if (m_dismissable) { Q_EMIT dismissRequested(m_internalId); } } void Notification::show() { m_ready = true; Q_EMIT ready(); if (!m_silent) { m_closed = false; m_notification->sendEvent(); } } void Notification::update(const NetworkPacket& np) { parseNetworkPacket(np); createKNotification(!m_closed, np); } KNotification* Notification::createKNotification(bool update, const NetworkPacket& np) { if (!update) { m_notification = new KNotification(QStringLiteral("notification"), KNotification::CloseOnTimeout, this); m_notification->setComponentName(QStringLiteral("kdeconnect")); } QString escapedTitle = m_title.toHtmlEscaped(); QString escapedText = m_text.toHtmlEscaped(); QString escapedTicker = m_ticker.toHtmlEscaped(); m_notification->setTitle(m_appName.toHtmlEscaped()); if (m_title.isEmpty() && m_text.isEmpty()) { m_notification->setText(escapedTicker); } else if (m_appName==m_title) { m_notification->setText(escapedText); } else if (m_title.isEmpty()){ m_notification->setText(escapedText); } else if (m_text.isEmpty()){ m_notification->setText(escapedTitle); } else { m_notification->setText(escapedTitle+": "+escapedText); } m_hasIcon = m_hasIcon && !m_payloadHash.isEmpty(); if (!m_hasIcon) { applyNoIcon(); show(); } else { m_iconPath = m_imagesDir.absoluteFilePath(m_payloadHash); loadIcon(np); } if (!m_requestReplyId.isEmpty()) { m_notification->setActions(QStringList(i18n("Reply"))); connect(m_notification, &KNotification::action1Activated, this, &Notification::reply); } connect(m_notification, &KNotification::closed, this, &Notification::closed); return m_notification; } void Notification::loadIcon(const NetworkPacket& np) { m_ready = false; if (QFileInfo::exists(m_iconPath)) { applyIcon(); show(); } else { FileTransferJob* fileTransferJob = s_downloadsInProgress.value(m_iconPath); if (!fileTransferJob) { fileTransferJob = np.createPayloadTransferJob(QUrl::fromLocalFile(m_iconPath)); fileTransferJob->start(); s_downloadsInProgress[m_iconPath] = fileTransferJob; } connect(fileTransferJob, &FileTransferJob::result, this, [this, fileTransferJob]{ s_downloadsInProgress.remove(m_iconPath); if (fileTransferJob->error()) { qCDebug(KDECONNECT_PLUGIN_NOTIFICATION) << "Error in FileTransferJob: " << fileTransferJob->errorString(); applyNoIcon(); } else { applyIcon(); } show(); }); } } void Notification::applyIcon() { QPixmap icon(m_iconPath, "PNG"); m_notification->setPixmap(icon); } void Notification::applyNoIcon() { - //HACK The only way to display no icon at all is trying to load a non-existant icon + //HACK The only way to display no icon at all is trying to load a non-existent icon m_notification->setIconName(QStringLiteral("not_a_real_icon")); } void Notification::reply() { Q_EMIT replyRequested(); } void Notification::closed() { m_closed = true; } void Notification::parseNetworkPacket(const NetworkPacket& np) { m_internalId = np.get(QStringLiteral("id")); m_appName = np.get(QStringLiteral("appName")); m_ticker = np.get(QStringLiteral("ticker")); m_title = np.get(QStringLiteral("title")); m_text = np.get(QStringLiteral("text")); m_dismissable = np.get(QStringLiteral("isClearable")); m_hasIcon = np.hasPayload(); m_silent = np.get(QStringLiteral("silent")); m_payloadHash = np.get(QStringLiteral("payloadHash")); m_requestReplyId = np.get(QStringLiteral("requestReplyId"), QString()); } diff --git a/plugins/notifications/notificationsdbusinterface.cpp b/plugins/notifications/notificationsdbusinterface.cpp index a8aa7faa..68304323 100644 --- a/plugins/notifications/notificationsdbusinterface.cpp +++ b/plugins/notifications/notificationsdbusinterface.cpp @@ -1,192 +1,192 @@ /** * Copyright 2013 Albert Vaca * * 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 . */ #include "notificationsdbusinterface.h" #include "notification_debug.h" #include "notification.h" #include #include #include #include "notificationsplugin.h" #include "sendreplydialog.h" //In older Qt released, qAsConst isnt available #include "qtcompat_p.h" NotificationsDbusInterface::NotificationsDbusInterface(KdeConnectPlugin* plugin) : QDBusAbstractAdaptor(const_cast(plugin->device())) , m_device(plugin->device()) , m_plugin(plugin) , m_lastId(0) { } NotificationsDbusInterface::~NotificationsDbusInterface() { qCDebug(KDECONNECT_PLUGIN_NOTIFICATION) << "Destroying NotificationsDbusInterface"; } void NotificationsDbusInterface::clearNotifications() { qDeleteAll(m_notifications); m_notifications.clear(); Q_EMIT allNotificationsRemoved(); } QStringList NotificationsDbusInterface::activeNotifications() { return m_notifications.keys(); } void NotificationsDbusInterface::notificationReady() { Notification* noti = static_cast(sender()); disconnect(noti, &Notification::ready, this, &NotificationsDbusInterface::notificationReady); addNotification(noti); } void NotificationsDbusInterface::processPacket(const NetworkPacket& np) { if (np.get(QStringLiteral("isCancel"))) { QString id = np.get(QStringLiteral("id")); // cut off kdeconnect-android's prefix if there: if (id.startsWith(QLatin1String("org.kde.kdeconnect_tp::"))) id = id.mid(id.indexOf(QLatin1String("::")) + 2); removeNotification(id); } else { QString id = np.get(QStringLiteral("id")); if (!m_internalIdToPublicId.contains(id)) { Notification* noti = new Notification(np, this); if (noti->isReady()) { addNotification(noti); } else { connect(noti, &Notification::ready, this, &NotificationsDbusInterface::notificationReady); } } else { QString pubId = m_internalIdToPublicId.value(id); Notification* noti = m_notifications.value(pubId); if (!noti) return; noti->update(np); if (noti->isReady()) { Q_EMIT notificationUpdated(pubId); } else { connect(noti, &Notification::ready, this, [this, pubId]{ Q_EMIT notificationUpdated(pubId); }); } } } } void NotificationsDbusInterface::addNotification(Notification* noti) { const QString& internalId = noti->internalId(); if (m_internalIdToPublicId.contains(internalId)) { removeNotification(internalId); } //qCDebug(KDECONNECT_PLUGIN_NOTIFICATION) << "addNotification" << internalId; connect(noti, &Notification::dismissRequested, this, &NotificationsDbusInterface::dismissRequested); connect(noti, &Notification::replyRequested, this, [this,noti]{ replyRequested(noti); }); const QString& publicId = newId(); m_notifications[publicId] = noti; m_internalIdToPublicId[internalId] = publicId; QDBusConnection::sessionBus().registerObject(m_device->dbusPath()+"/notifications/"+publicId, noti, QDBusConnection::ExportScriptableContents); Q_EMIT notificationPosted(publicId); } void NotificationsDbusInterface::removeNotification(const QString& internalId) { //qCDebug(KDECONNECT_PLUGIN_NOTIFICATION) << "removeNotification" << internalId; if (!m_internalIdToPublicId.contains(internalId)) { qCDebug(KDECONNECT_PLUGIN_NOTIFICATION) << "Not found noti by internal Id: " << internalId; return; } QString publicId = m_internalIdToPublicId.take(internalId); Notification* noti = m_notifications.take(publicId); if (!noti) { qCDebug(KDECONNECT_PLUGIN_NOTIFICATION) << "Not found noti by public Id: " << publicId; return; } //Deleting the notification will unregister it automatically //QDBusConnection::sessionBus().unregisterObject(mDevice->dbusPath()+"/notifications/"+publicId); noti->deleteLater(); Q_EMIT notificationRemoved(publicId); } void NotificationsDbusInterface::dismissRequested(const QString& internalId) { NetworkPacket np(PACKET_TYPE_NOTIFICATION_REQUEST); np.set(QStringLiteral("cancel"), internalId); m_plugin->sendPacket(np); - //Workaround: we erase notifications without waiting a repsonse from the + //Workaround: we erase notifications without waiting a response from the //phone because we won't receive a response if we are out of sync and this //notification no longer exists. Ideally, each time we reach the phone //after some time disconnected we should re-sync all the notifications. removeNotification(internalId); } void NotificationsDbusInterface::replyRequested(Notification* noti) { QString replyId = noti->replyId(); QString appName = noti->appName(); QString originalMessage = noti->ticker(); SendReplyDialog* dialog = new SendReplyDialog(originalMessage, replyId, appName); connect(dialog, &SendReplyDialog::sendReply, this, &NotificationsDbusInterface::sendReply); dialog->show(); dialog->raise(); } void NotificationsDbusInterface::sendReply(const QString& replyId, const QString& message) { NetworkPacket np(PACKET_TYPE_NOTIFICATION_REPLY); np.set(QStringLiteral("requestReplyId"), replyId); np.set(QStringLiteral("message"), message); m_plugin->sendPacket(np); } QString NotificationsDbusInterface::newId() { return QString::number(++m_lastId); } diff --git a/plugins/sendnotifications/notificationslistener.cpp b/plugins/sendnotifications/notificationslistener.cpp index 235a5d0a..b87d951c 100644 --- a/plugins/sendnotifications/notificationslistener.cpp +++ b/plugins/sendnotifications/notificationslistener.cpp @@ -1,277 +1,277 @@ /** * Copyright 2015 Holger Kaelberer * * 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 . */ -#include -#include -#include +#include +#include +#include #include #include #include #include #include #include #include #include #include #include #include "notificationslistener.h" #include "sendnotificationsplugin.h" #include "sendnotification_debug.h" #include "notifyingapplication.h" //In older Qt released, qAsConst isnt available #include "qtcompat_p.h" NotificationsListener::NotificationsListener(KdeConnectPlugin* aPlugin) : QDBusAbstractAdaptor(aPlugin), m_plugin(aPlugin) { qRegisterMetaTypeStreamOperators("NotifyingApplication"); bool ret = QDBusConnection::sessionBus() .registerObject(QStringLiteral("/org/freedesktop/Notifications"), this, QDBusConnection::ExportScriptableContents); if (!ret) qCWarning(KDECONNECT_PLUGIN_SENDNOTIFICATION) << "Error registering notifications listener for device" << m_plugin->device()->name() << ":" << QDBusConnection::sessionBus().lastError(); else qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATION) << "Registered notifications listener for device" << m_plugin->device()->name(); QDBusInterface iface(QStringLiteral("org.freedesktop.DBus"), QStringLiteral("/org/freedesktop/DBus"), QStringLiteral("org.freedesktop.DBus")); iface.call(QStringLiteral("AddMatch"), "interface='org.freedesktop.Notifications',member='Notify',type='method_call',eavesdrop='true'"); setTranslatedAppName(); loadApplications(); connect(m_plugin->config(), &KdeConnectPluginConfig::configChanged, this, &NotificationsListener::loadApplications); } NotificationsListener::~NotificationsListener() { qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATION) << "Destroying NotificationsListener"; QDBusInterface iface(QStringLiteral("org.freedesktop.DBus"), QStringLiteral("/org/freedesktop/DBus"), QStringLiteral("org.freedesktop.DBus")); QDBusMessage res = iface.call(QStringLiteral("RemoveMatch"), "interface='org.freedesktop.Notifications',member='Notify',type='method_call',eavesdrop='true'"); QDBusConnection::sessionBus().unregisterObject(QStringLiteral("/org/freedesktop/Notifications")); } void NotificationsListener::setTranslatedAppName() { QString filePath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("knotifications5/kdeconnect.notifyrc"), QStandardPaths::LocateFile); if (filePath.isEmpty()) { qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATION) << "Couldn't find kdeconnect.notifyrc to hide kdeconnect notifications on the devices. Using default name."; m_translatedAppName = QStringLiteral("KDE Connect"); return; } KConfig config(filePath, KConfig::OpenFlag::SimpleConfig); KConfigGroup globalgroup(&config, QStringLiteral("Global")); m_translatedAppName = globalgroup.readEntry(QStringLiteral("Name"), QStringLiteral("KDE Connect")); } void NotificationsListener::loadApplications() { m_applications.clear(); const QVariantList list = m_plugin->config()->getList(QStringLiteral("applications")); for (const auto& a : list) { NotifyingApplication app = a.value(); if (!m_applications.contains(app.name)) m_applications.insert(app.name, app); } //qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATION) << "Loaded" << applications.size() << " applications"; } bool NotificationsListener::parseImageDataArgument(const QVariant& argument, int& width, int& height, int& rowStride, int& bitsPerSample, int& channels, bool& hasAlpha, QByteArray& imageData) const { if (!argument.canConvert()) return false; const QDBusArgument dbusArg = argument.value(); dbusArg.beginStructure(); dbusArg >> width >> height >> rowStride >> hasAlpha >> bitsPerSample >> channels >> imageData; dbusArg.endStructure(); return true; } QSharedPointer NotificationsListener::iconForImageData(const QVariant& argument) const { int width, height, rowStride, bitsPerSample, channels; bool hasAlpha; QByteArray imageData; if (!parseImageDataArgument(argument, width, height, rowStride, bitsPerSample, channels, hasAlpha, imageData)) return QSharedPointer(); if (bitsPerSample != 8) { qCWarning(KDECONNECT_PLUGIN_SENDNOTIFICATION) << "Unsupported image format:" << "width=" << width << "height=" << height << "rowStride=" << rowStride << "bitsPerSample=" << bitsPerSample << "channels=" << channels << "hasAlpha=" << hasAlpha; return QSharedPointer(); } QImage image(reinterpret_cast(imageData.data()), width, height, rowStride, hasAlpha ? QImage::Format_ARGB32 : QImage::Format_RGB32); if (hasAlpha) image = image.rgbSwapped(); // RGBA --> ARGB QSharedPointer buffer = QSharedPointer(new QBuffer); if (!buffer || !buffer->open(QIODevice::WriteOnly) || !image.save(buffer.data(), "PNG")) { qCWarning(KDECONNECT_PLUGIN_SENDNOTIFICATION) << "Could not initialize image buffer"; return QSharedPointer(); } return buffer; } QSharedPointer NotificationsListener::iconForIconName(const QString& iconName) const { int size = KIconLoader::SizeEnormous; // use big size to allow for good // quality on high-DPI mobile devices QString iconPath = KIconLoader::global()->iconPath(iconName, -size, true); if (!iconPath.isEmpty()) { if (!iconPath.endsWith(QLatin1String(".png")) && KIconLoader::global()->theme()->name() != QLatin1String("hicolor")) { // try falling back to hicolor theme: KIconTheme hicolor(QStringLiteral("hicolor")); if (hicolor.isValid()) { iconPath = hicolor.iconPath(iconName + ".png", size, KIconLoader::MatchBest); //qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATION) << "Found non-png icon in default theme trying fallback to hicolor:" << iconPath; } } } if (iconPath.endsWith(QLatin1String(".png"))) return QSharedPointer(new QFile(iconPath)); return QSharedPointer(); } uint NotificationsListener::Notify(const QString& appName, uint replacesId, const QString& appIcon, const QString& summary, const QString& body, const QStringList& actions, const QVariantMap& hints, int timeout) { static int id = 0; Q_UNUSED(actions); //qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATION) << "Got notification appName=" << appName << "replacesId=" << replacesId << "appIcon=" << appIcon << "summary=" << summary << "body=" << body << "actions=" << actions << "hints=" << hints << "timeout=" << timeout; // skip our own notifications if (appName == m_translatedAppName) return 0; NotifyingApplication app; if (!m_applications.contains(appName)) { // new application -> add to config app.name = appName; app.icon = appIcon; app.active = true; app.blacklistExpression = QRegularExpression(); m_applications.insert(app.name, app); // update config: QVariantList list; for (const auto& a : qAsConst(m_applications)) list << QVariant::fromValue(a); m_plugin->config()->setList(QStringLiteral("applications"), list); //qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATION) << "Added new application to config:" << app; } else app = m_applications.value(appName); if (!app.active) return 0; if (timeout > 0 && m_plugin->config()->get(QStringLiteral("generalPersistent"), false)) return 0; int urgency = -1; if (hints.contains(QStringLiteral("urgency"))) { bool ok; urgency = hints[QStringLiteral("urgency")].toInt(&ok); if (!ok) urgency = -1; } if (urgency > -1 && urgency < m_plugin->config()->get(QStringLiteral("generalUrgency"), 0)) return 0; QString ticker = summary; if (!body.isEmpty() && m_plugin->config()->get(QStringLiteral("generalIncludeBody"), true)) ticker += QStringLiteral(": ") + body; if (app.blacklistExpression.isValid() && !app.blacklistExpression.pattern().isEmpty() && app.blacklistExpression.match(ticker).hasMatch()) return 0; //qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATION) << "Sending notification from" << appName << ":" < 0 ? replacesId : ++id)}, {"appName", appName}, {"ticker", ticker}, {"isClearable", timeout == 0} }); // KNotifications are persistent if // timeout == 0, for other notifications // clearability is pointless // sync any icon data? if (m_plugin->config()->get(QStringLiteral("generalSynchronizeIcons"), true)) { QSharedPointer iconSource; // try different image sources according to priorities in notifications- // spec version 1.2: if (hints.contains(QStringLiteral("image-data"))) iconSource = iconForImageData(hints[QStringLiteral("image-data")]); else if (hints.contains(QStringLiteral("image_data"))) // 1.1 backward compatibility iconSource = iconForImageData(hints[QStringLiteral("image_data")]); else if (hints.contains(QStringLiteral("image-path"))) iconSource = iconForIconName(hints[QStringLiteral("image-path")].toString()); else if (hints.contains(QStringLiteral("image_path"))) // 1.1 backward compatibility iconSource = iconForIconName(hints[QStringLiteral("image_path")].toString()); else if (!appIcon.isEmpty()) iconSource = iconForIconName(appIcon); else if (hints.contains(QStringLiteral("icon_data"))) // < 1.1 backward compatibility iconSource = iconForImageData(hints[QStringLiteral("icon_data")]); if (iconSource) np.setPayload(iconSource, iconSource->size()); } m_plugin->sendPacket(np); return (replacesId > 0 ? replacesId : id); } diff --git a/plugins/sendnotifications/notificationslistener.h b/plugins/sendnotifications/notificationslistener.h index adddfdc0..48b875a3 100644 --- a/plugins/sendnotifications/notificationslistener.h +++ b/plugins/sendnotifications/notificationslistener.h @@ -1,64 +1,64 @@ /** * Copyright 2015 Holger Kaelberer * * 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 . */ -#include -#include +#include +#include #include #include #include class KdeConnectPlugin; class Notification; struct NotifyingApplication; class NotificationsListener : public QDBusAbstractAdaptor { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.freedesktop.Notifications") public: explicit NotificationsListener(KdeConnectPlugin* aPlugin); ~NotificationsListener() override; protected: KdeConnectPlugin* m_plugin; QHash m_applications; // virtual helper function to make testing possible (QDBusArgument can not // be injected without making a DBUS-call): virtual bool parseImageDataArgument(const QVariant& argument, int& width, int& height, int& rowStride, int& bitsPerSample, int& channels, bool& hasAlpha, QByteArray& imageData) const; QSharedPointer iconForImageData(const QVariant& argument) const; QSharedPointer iconForIconName(const QString& iconName) const; public Q_SLOTS: Q_SCRIPTABLE uint Notify(const QString&, uint, const QString&, const QString&, const QString&, const QStringList&, const QVariantMap&, int); private Q_SLOTS: void loadApplications(); private: void setTranslatedAppName(); QString m_translatedAppName; }; diff --git a/plugins/sftp/sftpplugin.h b/plugins/sftp/sftpplugin.h index 8539e31d..32ca85be 100644 --- a/plugins/sftp/sftpplugin.h +++ b/plugins/sftp/sftpplugin.h @@ -1,77 +1,77 @@ /** * Copyright 2014 Samoilenko Yuri * * 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 . */ #ifndef SFTPPLUGIN_H #define SFTPPLUGIN_H #include #include #define PACKET_TYPE_SFTP_REQUEST QStringLiteral("kdeconnect.sftp.request") class SftpPlugin : public KdeConnectPlugin { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.kde.kdeconnect.device.sftp") public: explicit SftpPlugin(QObject* parent, const QVariantList& args); ~SftpPlugin() override; bool receivePacket(const NetworkPacket& np) override; void connected() override {} QString dbusPath() const override { return "/modules/kdeconnect/devices/" + deviceId + "/sftp"; } Q_SIGNALS: void packetReceived(const NetworkPacket& np); Q_SCRIPTABLE void mounted(); Q_SCRIPTABLE void unmounted(); public Q_SLOTS: Q_SCRIPTABLE void mount(); Q_SCRIPTABLE void unmount(); Q_SCRIPTABLE bool mountAndWait(); Q_SCRIPTABLE bool isMounted() const; Q_SCRIPTABLE bool startBrowsing(); Q_SCRIPTABLE QString mountPoint(); - Q_SCRIPTABLE QVariantMap getDirectories(); //Actually a QMap, but QDBus preffers this + Q_SCRIPTABLE QVariantMap getDirectories(); //Actually a QMap, but QDBus prefers this private Q_SLOTS: void onMounted(); void onUnmounted(); void onFailed(const QString& message); private: void knotify(int type, const QString& text, const QPixmap& icon) const; void addToDolphin(); void removeFromDolphin(); private: struct Pimpl; QScopedPointer d; QString deviceId; //Storing it to avoid accessing device() from the destructor which could cause a crash - QVariantMap remoteDirectories; //Actually a QMap, but QDBus preffers this + QVariantMap remoteDirectories; //Actually a QMap, but QDBus prefers this }; #endif diff --git a/smsapp/conversationlistmodel.cpp b/smsapp/conversationlistmodel.cpp index 1bbbcc54..0f905c76 100644 --- a/smsapp/conversationlistmodel.cpp +++ b/smsapp/conversationlistmodel.cpp @@ -1,169 +1,169 @@ /* * This file is part of KDE Telepathy Chat * * Copyright (C) 2018 Aleix Pol Gonzalez * Copyright (C) 2018 Simon Redman * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #include "conversationlistmodel.h" #include #include "interfaces/conversationmessage.h" Q_LOGGING_CATEGORY(KDECONNECT_SMS_CONVERSATIONS_LIST_MODEL, "kdeconnect.sms.conversations_list") ConversationListModel::ConversationListModel(QObject* parent) : QStandardItemModel(parent) , m_conversationsInterface(nullptr) { qCCritical(KDECONNECT_SMS_CONVERSATIONS_LIST_MODEL) << "Constructing" << this; auto roles = roleNames(); roles.insert(FromMeRole, "fromMe"); roles.insert(AddressRole, "address"); roles.insert(PersonUriRole, "personUri"); roles.insert(ConversationIdRole, "conversationId"); roles.insert(DateRole, "date"); setItemRoleNames(roles); ConversationMessage::registerDbusType(); } ConversationListModel::~ConversationListModel() { } void ConversationListModel::setDeviceId(const QString& deviceId) { qCCritical(KDECONNECT_SMS_CONVERSATIONS_LIST_MODEL) << "setDeviceId" << deviceId << "of" << this; if (deviceId == m_deviceId) return; if (m_conversationsInterface) { disconnect(m_conversationsInterface, SIGNAL(conversationCreated(QString)), this, SLOT(handleCreatedConversation(QString))); delete m_conversationsInterface; } m_deviceId = deviceId; m_conversationsInterface = new DeviceConversationsDbusInterface(deviceId, this); connect(m_conversationsInterface, SIGNAL(conversationCreated(QString)), this, SLOT(handleCreatedConversation(QString))); - connect(m_conversationsInterface, SIGNAL(conversationMessageReceived(QVariantMap, int)), this, SLOT(createRowFromMessage(QVariantMap, int))); + connect(m_conversationsInterface, SIGNAL(conversationMessageReceived(QVariantMap,int)), this, SLOT(createRowFromMessage(QVariantMap,int))); prepareConversationsList(); m_conversationsInterface->requestAllConversationThreads(); } void ConversationListModel::prepareConversationsList() { QDBusPendingReply validThreadIDsReply = m_conversationsInterface->activeConversations(); setWhenAvailable(validThreadIDsReply, [this](const QStringList& convs) { clear(); for (const QString& conversationId : convs) { handleCreatedConversation(conversationId); } }, this); } void ConversationListModel::handleCreatedConversation(const QString& conversationId) { m_conversationsInterface->requestConversation(conversationId, 0, 1); } void ConversationListModel::printDBusError(const QDBusError& error) { qCWarning(KDECONNECT_SMS_CONVERSATIONS_LIST_MODEL) << error; } QStandardItem * ConversationListModel::conversationForThreadId(qint32 threadId) { for(int i=0, c=rowCount(); idata(ConversationIdRole) == threadId) return it; } return nullptr; } void ConversationListModel::createRowFromMessage(const QVariantMap& msg, int row) { if (row != 0) return; const ConversationMessage message(msg); if (message.type() == -1) { // The Android side currently hacks in -1 if something weird comes up // TODO: Remove this hack when MMS support is implemented return; } bool toadd = false; QStandardItem* item = conversationForThreadId(message.threadID()); if (!item) { toadd = true; item = new QStandardItem(); QScopedPointer personData(lookupPersonByAddress(message.address())); if (personData) { item->setText(personData->name()); item->setIcon(QIcon(personData->photo())); item->setData(personData->personUri(), PersonUriRole); } else { item->setData(QString(), PersonUriRole); item->setText(message.address()); } item->setData(message.threadID(), ConversationIdRole); } item->setData(message.address(), AddressRole); item->setData(message.type() == ConversationMessage::MessageTypeSent, FromMeRole); item->setData(message.body(), Qt::ToolTipRole); item->setData(message.date(), DateRole); if (toadd) appendRow(item); } KPeople::PersonData* ConversationListModel::lookupPersonByAddress(const QString& address) { int rowIndex = 0; for (rowIndex = 0; rowIndex < m_people.rowCount(); rowIndex++) { const QString& uri = m_people.get(rowIndex, KPeople::PersonsModel::PersonUriRole).toString(); KPeople::PersonData* person = new KPeople::PersonData(uri); const QString& email = person->email(); const QString& phoneNumber = canonicalizePhoneNumber(person->contactCustomProperty("phoneNumber").toString()); if (address == email || canonicalizePhoneNumber(address) == phoneNumber) { qCDebug(KDECONNECT_SMS_CONVERSATIONS_LIST_MODEL) << "Matched" << address << "to" << person->name(); return person; } delete person; } return nullptr; } QString ConversationListModel::canonicalizePhoneNumber(const QString& phoneNumber) { QString toReturn(phoneNumber); toReturn = toReturn.remove(' '); toReturn = toReturn.remove('-'); toReturn = toReturn.remove('('); toReturn = toReturn.remove(')'); toReturn = toReturn.remove('+'); return toReturn; } diff --git a/smsapp/conversationmodel.cpp b/smsapp/conversationmodel.cpp index b1a8e2c7..675bf596 100644 --- a/smsapp/conversationmodel.cpp +++ b/smsapp/conversationmodel.cpp @@ -1,113 +1,113 @@ /* * This file is part of KDE Telepathy Chat * * Copyright (C) 2018 Aleix Pol Gonzalez * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #include "conversationmodel.h" #include #include "interfaces/conversationmessage.h" Q_LOGGING_CATEGORY(KDECONNECT_SMS_CONVERSATION_MODEL, "kdeconnect.sms.conversation") ConversationModel::ConversationModel(QObject* parent) : QStandardItemModel(parent) , m_conversationsInterface(nullptr) { auto roles = roleNames(); roles.insert(FromMeRole, "fromMe"); roles.insert(DateRole, "date"); setItemRoleNames(roles); } ConversationModel::~ConversationModel() { } QString ConversationModel::threadId() const { return m_threadId; } void ConversationModel::setThreadId(const QString &threadId) { if (m_threadId == threadId) return; m_threadId = threadId; clear(); if (!threadId.isEmpty()) { m_conversationsInterface->requestConversation(threadId, 0, 10); } } void ConversationModel::setDeviceId(const QString& deviceId) { if (deviceId == m_deviceId) return; qCDebug(KDECONNECT_SMS_CONVERSATION_MODEL) << "setDeviceId" << "of" << this; if (m_conversationsInterface) delete m_conversationsInterface; m_deviceId = deviceId; m_conversationsInterface = new DeviceConversationsDbusInterface(deviceId, this); - connect(m_conversationsInterface, SIGNAL(conversationMessageReceived(QVariantMap, int)), this, SLOT(createRowFromMessage(QVariantMap, int))); + connect(m_conversationsInterface, SIGNAL(conversationMessageReceived(QVariantMap,int)), this, SLOT(createRowFromMessage(QVariantMap,int))); connect(m_conversationsInterface, SIGNAL(conversationUpdated(QVariantMap)), this, SLOT(handleConversationUpdate(QVariantMap))); } void ConversationModel::sendReplyToConversation(const QString& message) { qCDebug(KDECONNECT_SMS_CONVERSATION_MODEL) << "Trying to send" << message << "to conversation with ID" << m_threadId; m_conversationsInterface->replyToConversation(m_threadId, message); } void ConversationModel::createRowFromMessage(const QVariantMap& msg, int pos) { const ConversationMessage message(msg); if (!(message.threadID() == m_threadId.toInt())) { // Because of the asynchronous nature of the current implementation of this model, if the // user clicks quickly between threads or for some other reason a message comes when we're // not expecting it, we should not display it in the wrong place qCDebug(KDECONNECT_SMS_CONVERSATION_MODEL) << "Got a message for a thread" << message.threadID() << "but we are currently viewing" << m_threadId << "Discarding."; return; } auto item = new QStandardItem; item->setText(message.body()); item->setData(message.type() == ConversationMessage::MessageTypeSent, FromMeRole); item->setData(message.date(), DateRole); insertRow(pos, item); } void ConversationModel::handleConversationUpdate(const QVariantMap& msg) { const ConversationMessage message(msg); if (!(message.threadID() == m_threadId.toInt())) { // If a conversation which we are not currently viewing was updated, discard the information qCDebug(KDECONNECT_SMS_CONVERSATION_MODEL) << "Saw update for thread" << message.threadID() << "but we are currently viewing" << m_threadId; return; } createRowFromMessage(msg, 0); } diff --git a/tests/lanlinkprovidertest.cpp b/tests/lanlinkprovidertest.cpp index 0bc17d08..89ae08dd 100644 --- a/tests/lanlinkprovidertest.cpp +++ b/tests/lanlinkprovidertest.cpp @@ -1,356 +1,356 @@ /** * Copyright 2015 Vineet Garg * * 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 . */ // This class tests the behaviour of the class LanLinkProvider, be sure to kill process kdeconnectd to avoid any port binding issues #include "../core/backends/lan/lanlinkprovider.h" #include "../core/backends/lan/server.h" #include "../core/backends/lan/socketlinereader.h" #include "../core/kdeconnectconfig.h" #include #include #include #include #include /* * This class tests the working of LanLinkProvider under different conditions that when identity packet is received over TCP, over UDP and same when the device is paired. * It depends on KdeConnectConfig since LanLinkProvider internally uses it. */ class LanLinkProviderTest : public QObject { Q_OBJECT public: explicit LanLinkProviderTest() : m_lanLinkProvider(true) { QStandardPaths::setTestModeEnabled(true); } public Q_SLOTS: void initTestCase(); private Q_SLOTS: void pairedDeviceTcpPacketReceived(); void pairedDeviceUdpPacketReceived(); void unpairedDeviceTcpPacketReceived(); void unpairedDeviceUdpPacketReceived(); private: const int TEST_PORT = 8520; // Add some private fields here LanLinkProvider m_lanLinkProvider; Server* m_server; SocketLineReader* m_reader; QUdpSocket* m_udpSocket; QString m_identityPacket; // Attributes for test device QString m_deviceId; QString m_name; QCA::PrivateKey m_privateKey; QSslCertificate m_certificate; QSslCertificate generateCertificate(QString&, QCA::PrivateKey&); void addTrustedDevice(); void removeTrustedDevice(); void setSocketAttributes(QSslSocket* socket); void testIdentityPacket(QByteArray& identityPacket); }; void LanLinkProviderTest::initTestCase() { removeTrustedDevice(); // Remove trusted device if left by chance by any test m_deviceId = QStringLiteral("testdevice"); m_name = QStringLiteral("Test Device"); m_privateKey = QCA::KeyGenerator().createRSA(2048); m_certificate = generateCertificate(m_deviceId, m_privateKey); m_lanLinkProvider.onStart(); m_identityPacket = QStringLiteral("{\"id\":1439365924847,\"type\":\"kdeconnect.identity\",\"body\":{\"deviceId\":\"testdevice\",\"deviceName\":\"Test Device\",\"protocolVersion\":6,\"deviceType\":\"phone\",\"tcpPort\":") + QString::number(TEST_PORT) + QStringLiteral("}}"); } void LanLinkProviderTest::pairedDeviceTcpPacketReceived() { KdeConnectConfig* kcc = KdeConnectConfig::instance(); addTrustedDevice(); QUdpSocket* mUdpServer = new QUdpSocket; bool b = mUdpServer->bind(QHostAddress::LocalHost, LanLinkProvider::UDP_PORT, QUdpSocket::ShareAddress); QVERIFY(b); QSignalSpy spy(mUdpServer, SIGNAL(readyRead())); m_lanLinkProvider.onNetworkChange(); QVERIFY(!spy.isEmpty() || spy.wait()); QByteArray datagram; datagram.resize(mUdpServer->pendingDatagramSize()); QHostAddress sender; mUdpServer->readDatagram(datagram.data(), datagram.size(), &sender); testIdentityPacket(datagram); QJsonDocument jsonDocument = QJsonDocument::fromJson(datagram); QJsonObject body = jsonDocument.object().value(QStringLiteral("body")).toObject(); int tcpPort = body.value(QStringLiteral("tcpPort")).toInt(); QSslSocket socket; QSignalSpy spy2(&socket, SIGNAL(connected())); socket.connectToHost(sender, tcpPort); QVERIFY(spy2.wait()); QVERIFY2(socket.isOpen(), "Socket disconnected immediately"); socket.write(m_identityPacket.toLatin1()); socket.waitForBytesWritten(2000); QSignalSpy spy3(&socket, SIGNAL(encrypted())); setSocketAttributes(&socket); socket.addCaCertificate(kcc->certificate()); socket.setPeerVerifyMode(QSslSocket::VerifyPeer); socket.setPeerVerifyName(kcc->name()); socket.startServerEncryption(); QVERIFY(spy3.wait()); QCOMPARE(socket.sslErrors().size(), 0); QVERIFY2(socket.isValid(), "Server socket disconnected"); QVERIFY2(socket.isEncrypted(), "Server socket not yet encrypted"); QVERIFY2(!socket.peerCertificate().isNull(), "Peer certificate is null"); removeTrustedDevice(); delete mUdpServer; } void LanLinkProviderTest::pairedDeviceUdpPacketReceived() { KdeConnectConfig* kcc = KdeConnectConfig::instance(); addTrustedDevice(); m_server = new Server(this); m_udpSocket = new QUdpSocket(this); m_server->listen(QHostAddress::LocalHost, TEST_PORT); QSignalSpy spy(m_server, SIGNAL(newConnection())); qint64 bytesWritten = m_udpSocket->writeDatagram(m_identityPacket.toLatin1(), QHostAddress::LocalHost, LanLinkProvider::UDP_PORT); // write an identity packet to udp socket here, we do not broadcast it here QCOMPARE(bytesWritten, m_identityPacket.size()); // We should have an incoming connection now, wait for incoming connection QVERIFY(!spy.isEmpty() || spy.wait()); QSslSocket* serverSocket = m_server->nextPendingConnection(); QVERIFY2(serverSocket != 0, "Server socket is null"); QVERIFY2(serverSocket->isOpen(), "Server socket already closed"); m_reader = new SocketLineReader(serverSocket, this); QSignalSpy spy2(m_reader, SIGNAL(readyRead())); QVERIFY(spy2.wait()); QByteArray receivedPacket = m_reader->readLine(); testIdentityPacket(receivedPacket); - // Received identiy packet from LanLinkProvider now start ssl + // Received identity packet from LanLinkProvider now start ssl QSignalSpy spy3(serverSocket, SIGNAL(encrypted())); QVERIFY(connect(serverSocket, static_cast(&QSslSocket::error), this, [](QAbstractSocket::SocketError error){ qDebug() << "error:" << error; })); setSocketAttributes(serverSocket); serverSocket->addCaCertificate(kcc->certificate()); serverSocket->setPeerVerifyMode(QSslSocket::VerifyPeer); serverSocket->setPeerVerifyName(kcc->deviceId()); serverSocket->startClientEncryption(); // Its TCP server. but SSL client QVERIFY(!serverSocket->isEncrypted()); spy3.wait(2000); qDebug() << "xxxxxxxxx" << serverSocket->sslErrors(); QCOMPARE(serverSocket->sslErrors().size(), 0); QVERIFY2(serverSocket->isValid(), "Server socket disconnected"); QVERIFY2(serverSocket->isEncrypted(), "Server socket not yet encrypted"); QVERIFY2(!serverSocket->peerCertificate().isNull(), "Peer certificate is null"); removeTrustedDevice(); delete m_server; delete m_udpSocket; } void LanLinkProviderTest::unpairedDeviceTcpPacketReceived() { QUdpSocket* mUdpServer = new QUdpSocket; bool b = mUdpServer->bind(QHostAddress::LocalHost, LanLinkProvider::UDP_PORT, QUdpSocket::ShareAddress); QVERIFY(b); QSignalSpy spy(mUdpServer, SIGNAL(readyRead())); m_lanLinkProvider.onNetworkChange(); QVERIFY(!spy.isEmpty() || spy.wait()); QByteArray datagram; datagram.resize(mUdpServer->pendingDatagramSize()); QHostAddress sender; mUdpServer->readDatagram(datagram.data(), datagram.size(), &sender); testIdentityPacket(datagram); QJsonDocument jsonDocument = QJsonDocument::fromJson(datagram); QJsonObject body = jsonDocument.object().value(QStringLiteral("body")).toObject(); int tcpPort = body.value(QStringLiteral("tcpPort")).toInt(); QSslSocket socket; QSignalSpy spy2(&socket, SIGNAL(connected())); socket.connectToHost(sender, tcpPort); QVERIFY(spy2.wait()); QVERIFY2(socket.isOpen(), "Socket disconnected immediately"); socket.write(m_identityPacket.toLatin1()); socket.waitForBytesWritten(2000); QSignalSpy spy3(&socket, SIGNAL(encrypted())); // We don't take care for sslErrors signal here, but signal will emit still we will get successful connection setSocketAttributes(&socket); socket.setPeerVerifyMode(QSslSocket::QueryPeer); socket.startServerEncryption(); QVERIFY(spy3.wait()); QVERIFY2(socket.isValid(), "Server socket disconnected"); QVERIFY2(socket.isEncrypted(), "Server socket not yet encrypted"); QVERIFY2(!socket.peerCertificate().isNull(), "Peer certificate is null"); delete mUdpServer; } void LanLinkProviderTest::unpairedDeviceUdpPacketReceived() { m_server = new Server(this); m_udpSocket = new QUdpSocket(this); m_server->listen(QHostAddress::LocalHost, TEST_PORT); QSignalSpy spy(m_server, &Server::newConnection); qint64 bytesWritten = m_udpSocket->writeDatagram(m_identityPacket.toLatin1(), QHostAddress::LocalHost, LanLinkProvider::UDP_PORT); // write an identity packet to udp socket here, we do not broadcast it here QCOMPARE(bytesWritten, m_identityPacket.size()); QVERIFY(!spy.isEmpty() || spy.wait()); QSslSocket* serverSocket = m_server->nextPendingConnection(); QVERIFY2(serverSocket != 0, "Server socket is null"); QVERIFY2(serverSocket->isOpen(), "Server socket already closed"); m_reader = new SocketLineReader(serverSocket, this); QSignalSpy spy2(m_reader, &SocketLineReader::readyRead); QVERIFY(spy2.wait()); QByteArray receivedPacket = m_reader->readLine(); QVERIFY2(!receivedPacket.isEmpty(), "Empty packet received"); testIdentityPacket(receivedPacket); // Received identity packet from LanLinkProvider now start ssl QSignalSpy spy3(serverSocket, SIGNAL(encrypted())); setSocketAttributes(serverSocket); serverSocket->setPeerVerifyMode(QSslSocket::QueryPeer); serverSocket->startClientEncryption(); // Its TCP server. but SSL client QVERIFY(spy3.wait()); QVERIFY2(serverSocket->isValid(), "Server socket disconnected"); QVERIFY2(serverSocket->isEncrypted(), "Server socket not yet encrypted"); QVERIFY2(!serverSocket->peerCertificate().isNull(), "Peer certificate is null"); delete m_server; delete m_udpSocket; } void LanLinkProviderTest::testIdentityPacket(QByteArray& identityPacket) { QJsonDocument jsonDocument = QJsonDocument::fromJson(identityPacket); QJsonObject jsonObject = jsonDocument.object(); QJsonObject body = jsonObject.value(QStringLiteral("body")).toObject(); QCOMPARE(jsonObject.value("type").toString(), QString("kdeconnect.identity")); QVERIFY2(body.contains("deviceName"), "Device name not found in identity packet"); QVERIFY2(body.contains("deviceId"), "Device id not found in identity packet"); QVERIFY2(body.contains("protocolVersion"), "Protocol version not found in identity packet"); QVERIFY2(body.contains("deviceType"), "Device type not found in identity packet"); } QSslCertificate LanLinkProviderTest::generateCertificate(QString& commonName, QCA::PrivateKey& privateKey) { QDateTime startTime = QDateTime::currentDateTime(); QDateTime endTime = startTime.addYears(10); QCA::CertificateInfo certificateInfo; certificateInfo.insert(QCA::CommonName,commonName); certificateInfo.insert(QCA::Organization,QStringLiteral("KDE")); certificateInfo.insert(QCA::OrganizationalUnit,QStringLiteral("Kde connect")); QCA::CertificateOptions certificateOptions(QCA::PKCS10); certificateOptions.setSerialNumber(10); certificateOptions.setInfo(certificateInfo); certificateOptions.setValidityPeriod(startTime, endTime); certificateOptions.setFormat(QCA::PKCS10); QSslCertificate certificate = QSslCertificate(QCA::Certificate(certificateOptions, privateKey).toPEM().toLatin1()); return certificate; } void LanLinkProviderTest::setSocketAttributes(QSslSocket* socket) { socket->setPrivateKey(QSslKey(m_privateKey.toPEM().toLatin1(), QSsl::Rsa)); socket->setLocalCertificate(m_certificate); } void LanLinkProviderTest::addTrustedDevice() { KdeConnectConfig* kcc = KdeConnectConfig::instance(); kcc->addTrustedDevice(m_deviceId, m_name, QStringLiteral("phone")); kcc->setDeviceProperty(m_deviceId, QStringLiteral("certificate"), QString::fromLatin1(m_certificate.toPem())); } void LanLinkProviderTest::removeTrustedDevice() { KdeConnectConfig* kcc = KdeConnectConfig::instance(); kcc->removeTrustedDevice(m_deviceId); } QTEST_GUILESS_MAIN(LanLinkProviderTest) #include "lanlinkprovidertest.moc" diff --git a/tests/testnotificationlistener.cpp b/tests/testnotificationlistener.cpp index 346432a1..bf781e6a 100644 --- a/tests/testnotificationlistener.cpp +++ b/tests/testnotificationlistener.cpp @@ -1,503 +1,503 @@ /** * Copyright 2015 Holger Kaelberer * * 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 . */ #include #include #include #include #include #include #include #include #include #include "core/daemon.h" #include "core/device.h" #include "core/kdeconnectplugin.h" #include "kdeconnect-version.h" #include "plugins/sendnotifications/sendnotificationsplugin.h" #include "plugins/sendnotifications/notificationslistener.h" #include "plugins/sendnotifications/notifyingapplication.h" // Tweaked NotificationsPlugin for testing class TestNotificationsPlugin : public SendNotificationsPlugin { Q_OBJECT public: explicit TestNotificationsPlugin(QObject* parent, const QVariantList& args) : SendNotificationsPlugin(parent, args) { } ~TestNotificationsPlugin() override = default; // allow to access notificationsListener for testing: NotificationsListener* getNotificationsListener() const { return notificationsListener; } void setNotificationsListener(NotificationsListener* value) { notificationsListener = value; } }; // Tweaked Device for testing: class TestDevice: public Device { Q_OBJECT private: int sentPackets; NetworkPacket* lastPacket; public: explicit TestDevice(QObject* parent, const QString& id) : Device (parent, id) , sentPackets(0) , lastPacket(nullptr) {} ~TestDevice() override { delete lastPacket; } int getSentPackets() const { return sentPackets; } NetworkPacket* getLastPacket() { return lastPacket; } void deleteLastPacket() { delete lastPacket; lastPacket = nullptr; } public Q_SLOTS: bool sendPacket(NetworkPacket& np) override { ++sentPackets; // copy packet manually to allow for inspection (can't use copy-constructor) deleteLastPacket(); lastPacket = new NetworkPacket(np.type()); Q_ASSERT(lastPacket); for (QVariantMap::ConstIterator iter = np.body().constBegin(); iter != np.body().constEnd(); iter++) lastPacket->set(iter.key(), iter.value()); lastPacket->setPayload(np.payload(), np.payloadSize()); return true; } }; // Tweaked NotificationsListener for testing: class TestedNotificationsListener: public NotificationsListener { Q_OBJECT public: explicit TestedNotificationsListener(KdeConnectPlugin* aPlugin) : NotificationsListener(aPlugin) {} ~TestedNotificationsListener() override {} QHash& getApplications() { return m_applications; } void setApplications(const QHash& value) { m_applications = value; } protected: bool parseImageDataArgument(const QVariant& argument, int& width, int& height, int& rowStride, int& bitsPerSample, int& channels, bool& hasAlpha, QByteArray& imageData) const override { width = argument.toMap().value(QStringLiteral("width")).toInt(); height = argument.toMap().value(QStringLiteral("height")).toInt(); rowStride = argument.toMap().value(QStringLiteral("rowStride")).toInt(); bitsPerSample = argument.toMap().value(QStringLiteral("bitsPerSample")).toInt(); channels = argument.toMap().value(QStringLiteral("channels")).toInt(); hasAlpha = argument.toMap().value(QStringLiteral("hasAlpha")).toBool(); imageData = argument.toMap().value(QStringLiteral("imageData")).toByteArray(); return true; } }; class TestNotificationListener : public QObject { Q_OBJECT public: TestNotificationListener() : plugin(nullptr) { QStandardPaths::setTestModeEnabled(true); } private Q_SLOTS: void testNotify(); private: TestNotificationsPlugin* plugin; }; void TestNotificationListener::testNotify() { // // set things up: // QString dId(QStringLiteral("testid")); TestDevice* d = new TestDevice(nullptr, dId); int proxiedNotifications = 0; QCOMPARE(proxiedNotifications, d->getSentPackets()); plugin = new TestNotificationsPlugin(this, QVariantList({ QVariant::fromValue(d), "notifications_plugin", {"kdeconnect.notification"}, "preferences-desktop-notification"})); QVERIFY(plugin->getNotificationsListener()); delete plugin->getNotificationsListener(); // inject our tweaked NotificationsListener: TestedNotificationsListener* listener = new TestedNotificationsListener(plugin); QVERIFY(listener); plugin->setNotificationsListener(listener); QCOMPARE(listener, plugin->getNotificationsListener()); // make sure config is default: plugin->config()->set(QStringLiteral("generalPersistent"), false); plugin->config()->set(QStringLiteral("generalIncludeBody"), true); plugin->config()->set(QStringLiteral("generalUrgency"), 0); QCOMPARE(plugin->config()->get("generalPersistent"), false); QCOMPARE(plugin->config()->get("generalIncludeBody"), true); QCOMPARE(plugin->config()->get("generalUrgency"), false); // applications are modified directly: listener->getApplications().clear(); QCOMPARE(listener->getApplications().count(), 0); // // Go !!! // uint replacesId = 99; uint retId; QString appName(QStringLiteral("some-appName")); QString body(QStringLiteral("some-body")); QString icon(QStringLiteral("some-icon")); QString summary(QStringLiteral("some-summary")); // regular Notify call that is synchronized ... retId = listener->Notify(appName, replacesId, icon, summary, body, {}, {{}}, 0); // ... should return replacesId, QCOMPARE(retId, replacesId); // ... have triggered sending a packet QCOMPARE(++proxiedNotifications, d->getSentPackets()); // ... with our properties, QCOMPARE(d->getLastPacket()->get("id"), replacesId); QCOMPARE(d->getLastPacket()->get("appName"), appName); QCOMPARE(d->getLastPacket()->get("ticker"), summary + ": " + body); QCOMPARE(d->getLastPacket()->get("isClearable"), true); QCOMPARE(d->getLastPacket()->hasPayload(), false); // ... and create a new application internally that is initialized correctly: QCOMPARE(listener->getApplications().count(), 1); QVERIFY(listener->getApplications().contains(appName)); QVERIFY(listener->getApplications()[appName].active); QCOMPARE(listener->getApplications()[appName].name, appName); QVERIFY(listener->getApplications()[appName].blacklistExpression.pattern().isEmpty()); QCOMPARE(listener->getApplications()[appName].name, appName); QCOMPARE(listener->getApplications()[appName].icon, icon); // another one, with other timeout and urgency values: QString appName2(QStringLiteral("some-appName2")); QString body2(QStringLiteral("some-body2")); QString icon2(QStringLiteral("some-icon2")); QString summary2(QStringLiteral("some-summary2")); retId = listener->Notify(appName2, replacesId+1, icon2, summary2, body2, {}, {{"urgency", 2}}, 10); QCOMPARE(retId, replacesId+1); QCOMPARE(++proxiedNotifications, d->getSentPackets()); QCOMPARE(d->getLastPacket()->get("id"), replacesId+1); QCOMPARE(d->getLastPacket()->get("appName"), appName2); QCOMPARE(d->getLastPacket()->get("ticker"), summary2 + ": " + body2); QCOMPARE(d->getLastPacket()->get("isClearable"), false); // timeout != 0 QCOMPARE(d->getLastPacket()->hasPayload(), false); QCOMPARE(listener->getApplications().count(), 2); QVERIFY(listener->getApplications().contains(appName2)); QVERIFY(listener->getApplications().contains(appName)); // if persistent-only is set, timeouts > 0 are not synced: plugin->config()->set(QStringLiteral("generalPersistent"), true); retId = listener->Notify(appName, replacesId, icon, summary, body, {}, {{}}, 1); QCOMPARE(retId, 0U); QCOMPARE(proxiedNotifications, d->getSentPackets()); retId = listener->Notify(appName2, replacesId, icon2, summary2, body2, {}, {{}}, 3); QCOMPARE(retId, 0U); QCOMPARE(proxiedNotifications, d->getSentPackets()); // but timeout == 0 is retId = listener->Notify(appName, replacesId, icon, summary, body, {}, {{}}, 0); QCOMPARE(retId, replacesId); QCOMPARE(++proxiedNotifications, d->getSentPackets()); plugin->config()->set(QStringLiteral("generalPersistent"), false); // if min-urgency is set, lower urgency levels are not synced: plugin->config()->set(QStringLiteral("generalUrgency"), 1); retId = listener->Notify(appName, replacesId, icon, summary, body, {}, {{"urgency", 0}}, 0); QCOMPARE(retId, 0U); QCOMPARE(proxiedNotifications, d->getSentPackets()); // equal urgency is retId = listener->Notify(appName, replacesId, icon, summary, body, {}, {{"urgency", 1}}, 0); QCOMPARE(retId, replacesId); QCOMPARE(++proxiedNotifications, d->getSentPackets()); // higher urgency as well retId = listener->Notify(appName, replacesId, icon, summary, body, {}, {{"urgency", 2}}, 0); QCOMPARE(retId, replacesId); QCOMPARE(++proxiedNotifications, d->getSentPackets()); plugin->config()->set(QStringLiteral("generalUrgency"), 0); // notifications for a deactivated application are not synced: QVERIFY(listener->getApplications().contains(appName)); listener->getApplications()[appName].active = false; QVERIFY(!listener->getApplications()[appName].active); retId = listener->Notify(appName, replacesId, icon, summary, body, {}, {{"urgency", 0}}, 0); QCOMPARE(retId, 0U); QCOMPARE(proxiedNotifications, d->getSentPackets()); // others are still: retId = listener->Notify(appName2, replacesId+1, icon2, summary2, body2, {}, {{}}, 0); QCOMPARE(retId, replacesId+1); QCOMPARE(++proxiedNotifications, d->getSentPackets()); // back to normal: listener->getApplications()[appName].active = true; QVERIFY(listener->getApplications()[appName].active); retId = listener->Notify(appName, replacesId, icon, summary, body, {}, {{}}, 0); QCOMPARE(retId, replacesId); QCOMPARE(++proxiedNotifications, d->getSentPackets()); // notifications with blacklisted subjects are not synced: QVERIFY(listener->getApplications().contains(appName)); listener->getApplications()[appName].blacklistExpression.setPattern(QStringLiteral("black[12]|foo(bar|baz)")); retId = listener->Notify(appName, replacesId, icon, QStringLiteral("summary black1"), body, {}, {{}}, 0); QCOMPARE(retId, 0U); QCOMPARE(proxiedNotifications, d->getSentPackets()); retId = listener->Notify(appName, replacesId, icon, QStringLiteral("summary foobar"), body, {}, {{}}, 0); QCOMPARE(retId, 0U); QCOMPARE(proxiedNotifications, d->getSentPackets()); // other subjects are synced: retId = listener->Notify(appName, replacesId, icon, QStringLiteral("summary foo"), body, {}, {{}}, 0); QCOMPARE(retId, replacesId); QCOMPARE(++proxiedNotifications, d->getSentPackets()); retId = listener->Notify(appName, replacesId, icon, QStringLiteral("summary black3"), body, {}, {{}}, 0); QCOMPARE(retId, replacesId); QCOMPARE(++proxiedNotifications, d->getSentPackets()); // also body is checked by blacklist if requested: plugin->config()->set(QStringLiteral("generalIncludeBody"), true); retId = listener->Notify(appName, replacesId, icon, summary, QStringLiteral("body black1"), {}, {{}}, 0); QCOMPARE(retId, 0U); QCOMPARE(proxiedNotifications, d->getSentPackets()); retId = listener->Notify(appName, replacesId, icon, summary, QStringLiteral("body foobaz"), {}, {{}}, 0); QCOMPARE(retId, 0U); QCOMPARE(proxiedNotifications, d->getSentPackets()); // body does not matter if inclusion was not requested: plugin->config()->set(QStringLiteral("generalIncludeBody"), false); retId = listener->Notify(appName, replacesId, icon, summary, QStringLiteral("body black1"), {}, {{}}, 0); QCOMPARE(retId, replacesId); QCOMPARE(++proxiedNotifications, d->getSentPackets()); // without body, also ticker value is different: QCOMPARE(d->getLastPacket()->get("ticker"), summary); retId = listener->Notify(appName, replacesId, icon, summary, QStringLiteral("body foobaz"), {}, {{}}, 0); QCOMPARE(retId, replacesId); QCOMPARE(++proxiedNotifications, d->getSentPackets()); // back to normal: listener->getApplications()[appName].blacklistExpression.setPattern(QLatin1String("")); plugin->config()->set(QStringLiteral("generalIncludeBody"), true); retId = listener->Notify(appName, replacesId, icon, summary, body, {}, {{}}, 0); QCOMPARE(retId, replacesId); QCOMPARE(++proxiedNotifications, d->getSentPackets()); retId = listener->Notify(appName2, replacesId, icon2, summary2, body2, {}, {{}}, 0); QCOMPARE(retId, replacesId); QCOMPARE(++proxiedNotifications, d->getSentPackets()); // icon synchronization: QStringList iconPaths; // appIcon int count = 0; const QStringList icons = KIconLoader::global()->queryIcons(-KIconLoader::SizeEnormous, KIconLoader::Application); for (const auto& iconName : icons) { if (!iconName.endsWith(QLatin1String(".png"))) continue; if (count++ > 3) // max 3 iterations break; iconPaths.append(iconName); // memorize some paths for later // existing icons are sync-ed if requested plugin->config()->set(QStringLiteral("generalSynchronizeIcons"), true); QFileInfo fi(iconName); retId = listener->Notify(appName, replacesId, fi.baseName(), summary, body, {}, {{}}, 0); QCOMPARE(retId, replacesId); QCOMPARE(++proxiedNotifications, d->getSentPackets()); QVERIFY(d->getLastPacket()->hasPayload()); QCOMPARE(d->getLastPacket()->payloadSize(), fi.size()); - // works also with abolute paths + // works also with absolute paths retId = listener->Notify(appName, replacesId, iconName, summary, body, {}, {{}}, 0); QCOMPARE(retId, replacesId); QCOMPARE(++proxiedNotifications, d->getSentPackets()); QVERIFY(d->getLastPacket()->hasPayload()); QCOMPARE(d->getLastPacket()->payloadSize(), fi.size()); // extensions other than png are not accepted: retId = listener->Notify(appName, replacesId, iconName + ".svg", summary, body, {}, {{}}, 0); QCOMPARE(retId, replacesId); QCOMPARE(++proxiedNotifications, d->getSentPackets()); QVERIFY(!d->getLastPacket()->hasPayload()); // if sync not requested no payload: plugin->config()->set(QStringLiteral("generalSynchronizeIcons"), false); retId = listener->Notify(appName, replacesId, fi.baseName(), summary, body, {}, {{}}, 0); QCOMPARE(retId, replacesId); QCOMPARE(++proxiedNotifications, d->getSentPackets()); QVERIFY(!d->getLastPacket()->hasPayload()); QCOMPARE(d->getLastPacket()->payloadSize(), 0); } plugin->config()->set(QStringLiteral("generalSynchronizeIcons"), true); // image-path in hints if (iconPaths.size() > 0) { retId = listener->Notify(appName, replacesId, iconPaths.size() > 1 ? iconPaths[1] : icon, summary, body, {}, {{"image-path", iconPaths[0]}}, 0); QCOMPARE(retId, replacesId); QCOMPARE(++proxiedNotifications, d->getSentPackets()); QVERIFY(d->getLastPacket()->hasPayload()); QFileInfo hintsFi(iconPaths[0]); // image-path has priority over appIcon parameter: QCOMPARE(d->getLastPacket()->payloadSize(), hintsFi.size()); } // image_path in hints if (iconPaths.size() > 0) { retId = listener->Notify(appName, replacesId, iconPaths.size() > 1 ? iconPaths[1] : icon, summary, body, {}, {{"image_path", iconPaths[0]}}, 0); QCOMPARE(retId, replacesId); QCOMPARE(++proxiedNotifications, d->getSentPackets()); QVERIFY(d->getLastPacket()->hasPayload()); QFileInfo hintsFi(iconPaths[0]); // image_path has priority over appIcon parameter: QCOMPARE(d->getLastPacket()->payloadSize(), hintsFi.size()); } // image-data in hints // set up: QBuffer* buffer; QImage image; int width = 2, height = 2, rowStride = 4*width, bitsPerSample = 8, channels = 4; bool hasAlpha = 1; char rawData[] = { 0x01, 0x02, 0x03, 0x04, // raw rgba data 0x11, 0x12, 0x13, 0x14, 0x21, 0x22, 0x23, 0x24, 0x31, 0x32, 0x33, 0x34 }; QVariantMap imageData = {{"width", width}, {"height", height}, {"rowStride", rowStride}, {"bitsPerSample", bitsPerSample}, {"channels", channels}, {"hasAlpha", hasAlpha}, {"imageData", QByteArray(rawData, sizeof(rawData))}}; QVariantMap hints; #define COMPARE_PIXEL(x, y) \ QCOMPARE(qRed(image.pixel(x,y)), (int)rawData[x*4 + y*rowStride + 0]); \ QCOMPARE(qGreen(image.pixel(x,y)), (int)rawData[x*4 + y*rowStride + 1]); \ QCOMPARE(qBlue(image.pixel(x,y)), (int)rawData[x*4 + y*rowStride + 2]); \ QCOMPARE(qAlpha(image.pixel(x,y)), (int)rawData[x*4 + y*rowStride + 3]); hints.insert(QStringLiteral("image-data"), imageData); if (iconPaths.size() > 0) hints.insert(QStringLiteral("image-path"), iconPaths[0]); retId = listener->Notify(appName, replacesId, icon, summary, body, {}, hints, 0); QCOMPARE(retId, replacesId); QCOMPARE(++proxiedNotifications, d->getSentPackets()); QVERIFY(d->getLastPacket()->hasPayload()); buffer = dynamic_cast(d->getLastPacket()->payload().data()); QCOMPARE(d->getLastPacket()->payloadSize(), buffer->size()); // image-data is attached as png data QVERIFY(image.loadFromData(reinterpret_cast(buffer->data().constData()), buffer->size(), "PNG")); // image-data has priority over image-path: QCOMPARE(image.byteCount(), rowStride*height); // rgba -> argb conversion was done correctly: COMPARE_PIXEL(0,0); COMPARE_PIXEL(1,0); COMPARE_PIXEL(0,1); COMPARE_PIXEL(1,1); // same for image_data in hints hints.clear(); hints.insert(QStringLiteral("image-data"), imageData); if (iconPaths.size() > 0) hints.insert(QStringLiteral("image_path"), iconPaths[0]); retId = listener->Notify(appName, replacesId, icon, summary, body, {}, hints, 0); QCOMPARE(retId, replacesId); QCOMPARE(++proxiedNotifications, d->getSentPackets()); QVERIFY(d->getLastPacket()->hasPayload()); buffer = dynamic_cast(d->getLastPacket()->payload().data()); QCOMPARE(d->getLastPacket()->payloadSize(), buffer->size()); // image-data is attached as png data QVERIFY(image.loadFromData(reinterpret_cast(buffer->data().constData()), buffer->size(), "PNG")); // image_data has priority over image_path/image-path: QCOMPARE(image.byteCount(), rowStride*height); // rgba -> argb conversion was done correctly: COMPARE_PIXEL(0,0); COMPARE_PIXEL(1,0); COMPARE_PIXEL(0,1); COMPARE_PIXEL(1,1); // same for icon_data, which has lowest priority hints.clear(); hints.insert(QStringLiteral("icon_data"), imageData); retId = listener->Notify(appName, replacesId, QLatin1String(""), summary, body, {}, hints, 0); QCOMPARE(retId, replacesId); QCOMPARE(++proxiedNotifications, d->getSentPackets()); QVERIFY(d->getLastPacket()); QVERIFY(d->getLastPacket()->hasPayload()); buffer = dynamic_cast(d->getLastPacket()->payload().data()); // image-data is attached as png data QVERIFY(image.loadFromData(reinterpret_cast(buffer->data().constData()), buffer->size(), "PNG")); QCOMPARE(image.byteCount(), rowStride*height); // rgba -> argb conversion was done correctly: COMPARE_PIXEL(0,0); COMPARE_PIXEL(1,0); COMPARE_PIXEL(0,1); COMPARE_PIXEL(1,1); #undef COMPARE_PIXEL } QTEST_GUILESS_MAIN(TestNotificationListener); #include "testnotificationlistener.moc"