KUnifiedPush

distributor.cpp
1/*
2 SPDX-FileCopyrightText: 2022 Volker Krause <vkrause@kde.org>
3 SPDX-License-Identifier: LGPL-2.0-or-later
4*/
5
6#include "distributor.h"
7#include "distributor1adaptor.h"
8#include "managementadaptor.h"
9
10#include "client.h"
11#include "connector1iface.h"
12#include "gotifypushprovider.h"
13#include "logging.h"
14#include "message.h"
15#include "mockpushprovider.h"
16#include "nextpushprovider.h"
17#include "ntfypushprovider.h"
18
19#include "../shared/unifiedpush-constants.h"
20
21#include <QDBusConnection>
22#include <QSettings>
23
24#include <QNetworkInformation>
25
26using namespace KUnifiedPush;
27
28Distributor::Distributor(QObject *parent)
29 : QObject(parent)
30{
31 qDBusRegisterMetaType<KUnifiedPush::ClientInfo>();
32 qDBusRegisterMetaType<QList<KUnifiedPush::ClientInfo>>();
33
34 // setup network status tracking
35 if (QNetworkInformation::loadBackendByFeatures(QNetworkInformation::Feature::Reachability)) {
36 connect(QNetworkInformation::instance(), &QNetworkInformation::reachabilityChanged, this, &Distributor::processNextCommand);
37 } else {
38 qCWarning(Log) << "No network state information available!" << QNetworkInformation::availableBackends();
39 }
40
41 // register at D-Bus
42 new Distributor1Adaptor(this);
43 QDBusConnection::sessionBus().registerObject(UP_DISTRIBUTOR_PATH, this);
44
45 new ManagementAdaptor(this);
46 QDBusConnection::sessionBus().registerObject(KDE_DISTRIBUTOR_MANAGEMENT_PATH, this);
47
48 // create and set up push provider
49 if (!setupPushProvider()) {
50 return;
51 }
52
53 // load previous clients
54 // TODO what happens to existing clients if the above failed?
55 QSettings settings;
56 const auto clientTokens = settings.value(QStringLiteral("Clients/Tokens"), QStringList()).toStringList();
57 m_clients.reserve(clientTokens.size());
58 for (const auto &token : clientTokens) {
59 auto client = Client::load(token, settings);
60 if (client.isValid()) {
61 m_clients.push_back(std::move(client));
62 }
63 }
64 qCDebug(Log) << m_clients.size() << "registered clients loaded";
65
66 // purge uninstalled apps
67 purgeUnavailableClients();
68
69 // connect to push provider if necessary
70 if (!m_clients.empty())
71 {
72 setStatus(DistributorStatus::NoNetwork);
73 Command cmd;
74 cmd.type = Command::Connect;
75 m_commandQueue.push_back(std::move(cmd));
76 } else {
77 setStatus(DistributorStatus::Idle);
78 }
79
80 processNextCommand();
81}
82
83Distributor::~Distributor() = default;
84
85QString Distributor::Register(const QString& serviceName, const QString& token, const QString &description, QString& registrationResultReason)
86{
87 qCDebug(Log) << serviceName << token;
88 const auto it = std::find_if(m_clients.begin(), m_clients.end(), [&token](const auto &client) {
89 return client.token == token;
90 });
91 if (it == m_clients.end()) {
92 qCDebug(Log) << "Registering new client";
93
94 // if this is the first client, connect to the push provider first
95 // this can involve first-time device registration that is a prerequisite for registering clients
96 if (m_clients.empty()) {
97 Command cmd;
98 cmd.type = Command::Connect;
99 m_commandQueue.push_back(std::move(cmd));
100 }
101
102 Command cmd;
103 cmd.type = Command::Register;
104 cmd.client.token = token;
105 cmd.client.serviceName = serviceName;
106 cmd.client.description = description;
107 setDelayedReply(true);
108 cmd.reply = message().createReply();
109 m_commandQueue.push_back(std::move(cmd));
110
111 processNextCommand();
112 return {};
113 }
114
115 qCDebug(Log) << "Registering known client";
116 (*it).activate();
117 (*it).connector().NewEndpoint((*it).token, (*it).endpoint);
118 registrationResultReason.clear();
119 return UP_REGISTER_RESULT_SUCCESS;
120}
121
122void Distributor::Unregister(const QString& token)
123{
124 qCDebug(Log) << token;
125 const auto it = std::find_if(m_clients.begin(), m_clients.end(), [&token](const auto &client) {
126 return client.token == token;
127 });
128 if (it == m_clients.end()) {
129 qCWarning(Log) << "Unregistration request for unknown client.";
130 return;
131 }
132
133 Command cmd;
134 cmd.type = Command::Unregister;
135 cmd.client = (*it);
136 m_commandQueue.push_back(std::move(cmd));
137 processNextCommand();
138}
139
140void Distributor::messageReceived(const Message &msg) const
141{
142 qCDebug(Log) << msg.clientRemoteId << msg.content;
143 const auto it = std::find_if(m_clients.begin(), m_clients.end(), [&msg](const auto &client) {
144 return client.remoteId == msg.clientRemoteId;
145 });
146 if (it == m_clients.end()) {
147 qCWarning(Log) << "Received message for unknown client";
148 return;
149 }
150
151 (*it).activate();
152 (*it).connector().Message((*it).token, msg.content, {});
153}
154
155void Distributor::clientRegistered(const Client &client, AbstractPushProvider::Error error, const QString &errorMsg)
156{
157 qCDebug(Log) << client.token << client.remoteId << client.serviceName << error << errorMsg;
158 switch (error) {
160 {
161 // TODO check whether we got an endpoint, otherwise report an error
162 m_clients.push_back(client);
163
164 QSettings settings;
165 client.store(settings);
166 settings.setValue(QStringLiteral("Clients/Tokens"), clientTokens());
167 Q_EMIT registeredClientsChanged();
168
169 client.connector().NewEndpoint(client.token, client.endpoint);
170
171 if (m_currentCommand.reply.type() != QDBusMessage::InvalidMessage) {
172 m_currentCommand.reply << QString::fromLatin1(UP_REGISTER_RESULT_SUCCESS) << QString();
173 QDBusConnection::sessionBus().send(m_currentCommand.reply);
174 }
175 break;
176 }
178 // retry
179 m_commandQueue.push_front(std::move(m_currentCommand));
180 break;
182 m_currentCommand.reply << QString::fromLatin1(UP_REGISTER_RESULT_FAILURE) << errorMsg;
183 QDBusConnection::sessionBus().send(m_currentCommand.reply);
184 break;
185 }
186
187 m_currentCommand = {};
188 processNextCommand();
189}
190
191void Distributor::clientUnregistered(const Client &client, AbstractPushProvider::Error error)
192{
193 qCDebug(Log) << client.token << client.remoteId << client.serviceName << error;
194 switch (error) {
196 client.connector().Unregistered(m_currentCommand.type == Command::Unregister ? QString() : client.token);
197 [[fallthrough]];
199 {
200 QSettings settings;
201 settings.remove(client.token);
202 const auto it = std::find_if(m_clients.begin(), m_clients.end(), [&client](const auto &c) {
203 return c.token == client.token;
204 });
205 if (it != m_clients.end()) {
206 m_clients.erase(it);
207
208 // if this was the last client, also disconnect from the push provider
209 if (m_clients.empty()) {
210 Command cmd;
211 cmd.type = Command::Disconnect;
212 m_commandQueue.push_back(std::move(cmd));
213 }
214 }
215 settings.setValue(QStringLiteral("Clients/Tokens"), clientTokens());
216 Q_EMIT registeredClientsChanged();
217 break;
218 }
220 // retry
221 m_commandQueue.push_front(std::move(m_currentCommand));
222 break;
223 }
224
225 m_currentCommand = {};
226 processNextCommand();
227}
228
229void Distributor::providerConnected()
230{
231 qCDebug(Log);
232 setStatus(DistributorStatus::Connected);
233 m_currentCommand = {};
234 processNextCommand();
235}
236
237void Distributor::providerDisconnected(AbstractPushProvider::Error error, const QString &errorMsg)
238{
239 qCDebug(Log) << error << errorMsg;
240 if (m_currentCommand.type == Command::Disconnect) {
241 m_currentCommand = {};
242 setStatus(m_clients.empty() ? DistributorStatus::Idle : DistributorStatus::NoNetwork);
243 } else {
244 setStatus(DistributorStatus::NoNetwork);
245 }
246 processNextCommand();
247}
248
249QStringList Distributor::clientTokens() const
250{
251 QStringList l;
252 l.reserve(m_clients.size());
253 std::transform(m_clients.begin(), m_clients.end(), std::back_inserter(l), [](const auto &client) { return client.token; });
254 return l;
255}
256
257bool Distributor::setupPushProvider()
258{
259 // determine push provider
260 const auto pushProviderName = pushProviderId();
261 if (pushProviderName == GotifyPushProvider::Id) {
262 m_pushProvider.reset(new GotifyPushProvider);
263 } else if (pushProviderName == NextPushProvider::Id) {
264 m_pushProvider.reset(new NextPushProvider);
265 } else if (pushProviderName == NtfyPushProvider::Id) {
266 m_pushProvider.reset(new NtfyPushProvider);
267 } else if (pushProviderName == MockPushProvider::Id) {
268 m_pushProvider.reset(new MockPushProvider);
269 } else {
270 qCWarning(Log) << "Unknown push provider:" << pushProviderName;
271 m_pushProvider.reset();
272 setStatus(DistributorStatus::NoSetup);
273 return false;
274 }
275
276 QSettings settings;
277 settings.beginGroup(pushProviderName);
278 if (!m_pushProvider->loadSettings(settings)) {
279 qCWarning(Log) << "Invalid push provider settings!";
280 setStatus(DistributorStatus::NoSetup);
281 return false;
282 }
283 settings.endGroup();
284
285 connect(m_pushProvider.get(), &AbstractPushProvider::messageReceived, this, &Distributor::messageReceived);
286 connect(m_pushProvider.get(), &AbstractPushProvider::clientRegistered, this, &Distributor::clientRegistered);
287 connect(m_pushProvider.get(), &AbstractPushProvider::clientUnregistered, this, &Distributor::clientUnregistered);
288 connect(m_pushProvider.get(), &AbstractPushProvider::connected, this, &Distributor::providerConnected);
289 connect(m_pushProvider.get(), &AbstractPushProvider::disconnected, this, &Distributor::providerDisconnected);
290 return true;
291}
292
293void Distributor::purgeUnavailableClients()
294{
295 QStringList activatableServiceNames = QDBusConnection::sessionBus().interface()->activatableServiceNames();
296 std::sort(activatableServiceNames.begin(), activatableServiceNames.end());
297
298 // collect clients to unregister first, so m_clients doesn't change underneath us
299 QStringList tokensToUnregister;
300 for (const auto &client : m_clients) {
301 if (!std::binary_search(activatableServiceNames.begin(), activatableServiceNames.end(), client.serviceName)) {
302 tokensToUnregister.push_back(client.token);
303 }
304 }
305
306 // in mock mode assume everything is activatable, as unit tests don't install
307 // D-Bus service files
308 if (m_pushProvider->metaObject() == &MockPushProvider::staticMetaObject) [[unlikely]] {
309 return;
310 }
311
312 for (const auto &token : tokensToUnregister) {
313 Unregister(token);
314 }
315}
316
317bool Distributor::hasCurrentCommand() const
318{
319 return m_currentCommand.type != Command::NoCommand;
320}
321
322void Distributor::processNextCommand()
323{
324 if (hasCurrentCommand() || m_commandQueue.empty() || !isNetworkAvailable()) {
325 return;
326 }
327
328 m_currentCommand = m_commandQueue.front();
329 m_commandQueue.pop_front();
330 switch (m_currentCommand.type) {
331 case Command::NoCommand:
332 Q_ASSERT(false);
333 processNextCommand();
334 break;
335 case Command::Register:
336 m_pushProvider->registerClient(m_currentCommand.client);
337 break;
340 m_pushProvider->unregisterClient(m_currentCommand.client);
341 break;
342 case Command::Connect:
343 m_pushProvider->connectToProvider();
344 break;
345 case Command::Disconnect:
346 m_pushProvider->disconnectFromProvider();
347 break;
348 case Command::ChangePushProvider:
349 {
350 QSettings settings;
351 settings.setValue(QLatin1String("PushProvider/Type"), m_currentCommand.pushProvider);
352 m_currentCommand = {};
353 if (setupPushProvider()) {
354 processNextCommand();
355 }
356 break;
357 }
358 }
359}
360
361int Distributor::status() const
362{
363 return m_status;
364}
365
366void Distributor::setStatus(DistributorStatus::Status status)
367{
368 if (m_status == status) {
369 return;
370 }
371
372 m_status = status;
373 Q_EMIT statusChanged();
374}
375
376QString Distributor::pushProviderId() const
377{
378 QSettings settings;
379 return settings.value(QStringLiteral("PushProvider/Type"), QString()).toString();
380}
381
382QVariantMap Distributor::pushProviderConfiguration(const QString &pushProviderId) const
383{
384 if (pushProviderId.isEmpty()) {
385 return {};
386 }
387
388 QSettings settings;
389 settings.beginGroup(pushProviderId);
390 const auto keys = settings.allKeys();
391
392 QVariantMap config;
393 for (const auto &key : keys) {
394 const auto v = settings.value(key);
395 if (v.isValid()) {
396 config.insert(key, settings.value(key));
397 }
398 }
399
400 return config;
401}
402
403void Distributor::setPushProvider(const QString &pushProviderId, const QVariantMap &config)
404{
405 // store push provider config and check for changes
406 bool configChanged = false;
407 QSettings settings;
408 settings.beginGroup(pushProviderId);
409 for (auto it = config.begin(); it != config.end(); ++it) {
410 const auto oldValue = settings.value(it.key());
411 configChanged |= oldValue != it.value();
412 settings.setValue(it.key(), it.value());
413 }
414 settings.endGroup();
415 if (!configChanged && pushProviderId == this->pushProviderId()) {
416 return; // nothing changed
417 }
418
419 // if push provider or config changed: unregister all clients, create new push provider backend, re-register all clients
420 if (m_status != DistributorStatus::NoSetup) {
421 for (const auto &client : m_clients) {
422 forceUnregisterClient(client.token);
423 }
424 if (m_status == DistributorStatus::Connected) {
425 Command cmd;
426 cmd.type = Command::Disconnect;
427 m_commandQueue.push_back(std::move(cmd));
428 }
429 {
430 Command cmd;
431 cmd.type = Command::ChangePushProvider;
432 cmd.pushProvider = pushProviderId;
433 m_commandQueue.push_back(std::move(cmd));
434 }
435
436 // reconnect if there are clients
437 if (!m_clients.empty()) {
438 Command cmd;
439 cmd.type = Command::Connect;
440 m_commandQueue.push_back(std::move(cmd));
441 }
442
443 // re-register clients
444 for (const auto &client : m_clients) {
445 Command cmd;
446 cmd.type = Command::Register;
447 cmd.client = client;
448 m_commandQueue.push_back(std::move(cmd));
449 }
450 } else {
451 // recover from a previously failed attempt to change push providers
452
453 // reconnect if there are clients
454 if (!m_commandQueue.empty()) {
455 Command cmd;
456 cmd.type = Command::Connect;
457 m_commandQueue.push_front(std::move(cmd));
458 }
459
460 Command cmd;
461 cmd.type = Command::ChangePushProvider;
462 cmd.pushProvider = pushProviderId;
463 m_commandQueue.push_front(std::move(cmd));
464 }
465
466 processNextCommand();
467}
468
469QList<KUnifiedPush::ClientInfo> Distributor::registeredClients() const
470{
471 QList<KUnifiedPush::ClientInfo> result;
472 result.reserve(m_clients.size());
473
474 for (const auto &client : m_clients) {
475 ClientInfo info;
476 info.token = client.token;
477 info.serviceName = client.serviceName;
478 info.description = client.description;
479 result.push_back(std::move(info));
480 }
481
482 return result;
483}
484
485void Distributor::forceUnregisterClient(const QString &token)
486{
487 qCDebug(Log) << token;
488 const auto it = std::find_if(m_clients.begin(), m_clients.end(), [&token](const auto &client) {
489 return client.token == token;
490 });
491 if (it == m_clients.end()) {
492 qCWarning(Log) << "Unregistration request for unknown client.";
493 return;
494 }
495
496 Command cmd;
497 cmd.type = Command::ForceUnregister;
498 cmd.client = (*it);
499 m_commandQueue.push_back(std::move(cmd));
500 processNextCommand();
501}
502
503bool Distributor::isNetworkAvailable() const
504{
505 // if in doubt assume we have network and try to connect
507 const auto reachability = QNetworkInformation::instance()->reachability();
508 return reachability == QNetworkInformation::Reachability::Online || reachability == QNetworkInformation::Reachability::Unknown;
509 }
510 return true;
511}
512
513#include "moc_distributor.cpp"
void messageReceived(const KUnifiedPush::Message &msg)
Inform about a received push notification.
void connected()
Emitted after the connection to the push provider has been established successfully.
void clientUnregistered(const KUnifiedPush::Client &client, KUnifiedPush::AbstractPushProvider::Error error=NoError)
Emitted after successful client unregistration.
void disconnected(KUnifiedPush::AbstractPushProvider::Error error, const QString &errorMsg={})
Emitted after the connection to the push provider disconnected or failed to be established.
void clientRegistered(const KUnifiedPush::Client &client, KUnifiedPush::AbstractPushProvider::Error error=NoError, const QString &errorMsg={})
Emitted after successful client registration.
@ ProviderRejected
communication worked, but the provider refused to complete the operation
@ TransientNetworkError
temporary network error, try again
Information about a registered client.
Definition client.h:19
OrgUnifiedpushConnector1Interface connector() const
D-Bus UnifiedPush connector interface.
Definition client.cpp:52
Distributor command queue entries.
Definition command.h:17
@ Unregister
unregistration requested by client
Definition command.h:22
@ ForceUnregister
unregistration triggered by distributor
Definition command.h:23
A received push notification message.
Definition message.h:15
Q_SCRIPTABLE CaptureState status()
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
Client-side integration with UnifiedPush.
Definition connector.h:14
QDBusConnectionInterface * interface() const const
bool registerObject(const QString &path, QObject *object, RegisterOptions options)
bool send(const QDBusMessage &message) const const
QDBusConnection sessionBus()
const QDBusMessage & message() const const
void setDelayedReply(bool enable) const const
QDBusMessage createReply(const QList< QVariant > &arguments) const const
iterator begin()
iterator end()
void push_back(parameter_type value)
void reserve(qsizetype size)
QStringList availableBackends()
QNetworkInformation * instance()
bool loadBackendByFeatures(Features features)
void reachabilityChanged(Reachability newReachability)
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
QStringList allKeys() const const
void beginGroup(QAnyStringView prefix)
void endGroup()
void remove(QAnyStringView key)
void setValue(QAnyStringView key, const QVariant &value)
QVariant value(QAnyStringView key) const const
void clear()
QString fromLatin1(QByteArrayView str)
void push_back(QChar ch)
void push_front(QChar ch)
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
QString toString() const const
QStringList toStringList() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Mar 14 2025 12:00:33 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.