1 /*
2     SPDX-FileCopyrightText: 2016, 2019 Kai Uwe Broulik <kde@privat.broulik.de>
3 
4     SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include "smartlauncherbackend.h"
8 
9 #include <QDBusConnection>
10 #include <QDBusMessage>
11 #include <QDBusServiceWatcher>
12 #include <QDebug>
13 
14 #include <KConfigGroup>
15 #include <KService>
16 #include <KSharedConfig>
17 
18 #include <algorithm>
19 
20 #include <notificationmanager/settings.h>
21 
22 using namespace SmartLauncher;
23 using namespace NotificationManager;
24 
Backend(QObject * parent)25 Backend::Backend(QObject *parent)
26     : QObject(parent)
27     , m_watcher(new QDBusServiceWatcher(this))
28     , m_settings(new Settings(this))
29 {
30     setupUnity();
31 
32     reload();
33     connect(m_settings, &Settings::settingsChanged, this, &Backend::reload);
34 }
35 
36 Backend::~Backend() = default;
37 
reload()38 void Backend::reload()
39 {
40     m_badgeBlacklist = m_settings->badgeBlacklistedApplications();
41 
42     // Unity Launcher API operates on storage IDs ("foo.desktop"), whereas settings return desktop entries "foo"
43     std::transform(m_badgeBlacklist.begin(), m_badgeBlacklist.end(), m_badgeBlacklist.begin(), [](const QString &desktopEntry) -> QString {
44         return desktopEntry + QStringLiteral(".desktop");
45     });
46 
47     setupApplicationJobs();
48 
49     Q_EMIT reloadRequested(QString() /*all*/);
50 }
51 
doNotDisturbMode() const52 bool Backend::doNotDisturbMode() const
53 {
54     return m_settings->notificationsInhibitedByApplication()
55         || (m_settings->notificationsInhibitedUntil().isValid() && m_settings->notificationsInhibitedUntil() > QDateTime::currentDateTimeUtc());
56 }
57 
setupUnity()58 void Backend::setupUnity()
59 {
60     auto sessionBus = QDBusConnection::sessionBus();
61 
62     if (!sessionBus.connect({},
63                             {},
64                             QStringLiteral("com.canonical.Unity.LauncherEntry"),
65                             QStringLiteral("Update"),
66                             this,
67                             SLOT(update(QString, QMap<QString, QVariant>)))) {
68         qWarning() << "failed to register Update signal";
69         return;
70     }
71 
72     if (!sessionBus.registerObject(QStringLiteral("/Unity"), this)) {
73         qWarning() << "Failed to register unity object";
74         return;
75     }
76 
77     if (!sessionBus.registerService(QStringLiteral("com.canonical.Unity"))) {
78         qWarning() << "Failed to register unity service";
79         // In case an external process uses this (e.g. Latte Dock), let it just listen.
80     }
81 
82     KConfigGroup grp(KSharedConfig::openConfig(QStringLiteral("taskmanagerrulesrc")), QStringLiteral("Unity Launcher Mapping"));
83 
84     foreach (const QString &key, grp.keyList()) {
85         const QString &value = grp.readEntry(key, QString());
86         if (value.isEmpty()) {
87             continue;
88         }
89 
90         m_unityMappingRules.insert(key, value);
91     }
92 }
93 
setupApplicationJobs()94 void Backend::setupApplicationJobs()
95 {
96     if (m_settings->jobsInTaskManager() && !m_jobsModel) {
97         m_jobsModel = JobsModel::createJobsModel();
98         m_jobsModel->init();
99     } else if (!m_settings->jobsInTaskManager() && m_jobsModel) {
100         m_jobsModel = nullptr;
101     }
102 }
103 
hasLauncher(const QString & storageId) const104 bool Backend::hasLauncher(const QString &storageId) const
105 {
106     return m_launchers.contains(storageId);
107 }
108 
count(const QString & uri) const109 int Backend::count(const QString &uri) const
110 {
111     if (!m_settings->badgesInTaskManager() || doNotDisturbMode() || m_badgeBlacklist.contains(uri)) {
112         return 0;
113     }
114     return m_launchers.value(uri).count;
115 }
116 
countVisible(const QString & uri) const117 bool Backend::countVisible(const QString &uri) const
118 {
119     if (!m_settings->badgesInTaskManager() || doNotDisturbMode() || m_badgeBlacklist.contains(uri)) {
120         return false;
121     }
122     return m_launchers.value(uri).countVisible;
123 }
124 
progress(const QString & uri) const125 int Backend::progress(const QString &uri) const
126 {
127     if (!m_settings->jobsInTaskManager()) {
128         return 0;
129     }
130     return m_launchers.value(uri).progress;
131 }
132 
progressVisible(const QString & uri) const133 bool Backend::progressVisible(const QString &uri) const
134 {
135     if (!m_settings->jobsInTaskManager()) {
136         return false;
137     }
138     return m_launchers.value(uri).progressVisible;
139 }
140 
urgent(const QString & uri) const141 bool Backend::urgent(const QString &uri) const
142 {
143     return m_launchers.value(uri).urgent;
144 }
145 
unityMappingRules() const146 QHash<QString, QString> Backend::unityMappingRules() const
147 {
148     return m_unityMappingRules;
149 }
150 
update(const QString & uri,const QMap<QString,QVariant> & properties)151 void Backend::update(const QString &uri, const QMap<QString, QVariant> &properties)
152 {
153     Q_ASSERT(calledFromDBus());
154 
155     QString storageId;
156 
157     auto foundStorageId = m_launcherUrlToStorageId.constFind(uri);
158     if (foundStorageId == m_launcherUrlToStorageId.constEnd()) { // we don't know this one, register
159         // According to Unity Launcher API documentation applications *should* send along their
160         // desktop file name with application:// prefix
161         const QString applicationSchemePrefix = QStringLiteral("application://");
162 
163         QString normalizedUri = uri;
164         if (normalizedUri.startsWith(applicationSchemePrefix)) {
165             normalizedUri = uri.mid(applicationSchemePrefix.length());
166         }
167 
168         KService::Ptr service = KService::serviceByStorageId(normalizedUri);
169         if (!service) {
170             qWarning() << "Failed to find service for Unity Launcher" << uri;
171             return;
172         }
173 
174         storageId = service->storageId();
175         m_launcherUrlToStorageId.insert(uri, storageId);
176 
177         m_dbusServiceToLauncherUrl.insert(message().service(), uri);
178         m_watcher->addWatchedService(message().service());
179     } else {
180         storageId = *foundStorageId;
181     }
182 
183     auto foundEntry = m_launchers.find(storageId);
184     if (foundEntry == m_launchers.end()) { // we don't have it yet, create a new Entry
185         Entry entry;
186         foundEntry = m_launchers.insert(storageId, entry);
187     }
188 
189     auto propertiesEnd = properties.constEnd();
190 
191     auto foundCount = properties.constFind(QStringLiteral("count"));
192     if (foundCount != propertiesEnd) {
193         qint64 newCount = foundCount->toLongLong();
194         // 2 billion unread emails ought to be enough for anybody
195         if (newCount < std::numeric_limits<int>::max()) {
196             int saneCount = static_cast<int>(newCount);
197             if (saneCount != foundEntry->count) {
198                 foundEntry->count = saneCount;
199                 Q_EMIT countChanged(storageId, saneCount);
200             }
201         }
202     }
203 
204     updateLauncherProperty(storageId, properties, QStringLiteral("count"), &foundEntry->count, &Backend::count, &Backend::countChanged);
205     updateLauncherProperty(storageId,
206                            properties,
207                            QStringLiteral("count-visible"),
208                            &foundEntry->countVisible,
209                            &Backend::countVisible,
210                            &Backend::countVisibleChanged);
211 
212     // the API gives us progress as 0..1 double but we'll use percent to avoid unnecessary
213     // changes when it just changed a fraction of a percent, hence not using our fancy updateLauncherProperty method
214     auto foundProgress = properties.constFind(QStringLiteral("progress"));
215     if (foundProgress != propertiesEnd) {
216         const int oldSanitizedProgress = progress(storageId);
217 
218         foundEntry->progress = qRound(foundProgress->toDouble() * 100);
219 
220         const int newSanitizedProgress = progress(storageId);
221 
222         if (oldSanitizedProgress != newSanitizedProgress) {
223             Q_EMIT progressChanged(storageId, newSanitizedProgress);
224         }
225     }
226 
227     updateLauncherProperty(storageId,
228                            properties,
229                            QStringLiteral("progress-visible"),
230                            &foundEntry->progressVisible,
231                            &Backend::progressVisible,
232                            &Backend::progressVisibleChanged);
233     updateLauncherProperty(storageId, properties, QStringLiteral("urgent"), &foundEntry->urgent, &Backend::urgent, &Backend::urgentChanged);
234 }
235 
onServiceUnregistered(const QString & service)236 void Backend::onServiceUnregistered(const QString &service)
237 {
238     const QString &launcherUrl = m_dbusServiceToLauncherUrl.take(service);
239     if (launcherUrl.isEmpty()) {
240         return;
241     }
242 
243     const QString &storageId = m_launcherUrlToStorageId.take(launcherUrl);
244     if (storageId.isEmpty()) {
245         return;
246     }
247 
248     m_launchers.remove(storageId);
249     Q_EMIT launcherRemoved(storageId);
250 }
251