PlasmaActivitiesStats

resultwatcher.cpp
1/*
2 SPDX-FileCopyrightText: 2015, 2016 Ivan Cukic <ivan.cukic(at)kde.org>
3
4 SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
5*/
6
7#include "resultwatcher.h"
8
9// Qt
10#include <QCoreApplication>
11#include <QList>
12#include <QRegularExpression>
13#include <QSqlError>
14#include <QSqlQuery>
15
16// Local
17#include "plasma-activities-stats-logsettings.h"
18#include <common/database/Database.h>
19#include <utils/debug_and_return.h>
20
21// STL
22#include <functional>
23#include <iterator>
24#include <limits>
25#include <mutex>
26
27// PlasmaActivities
28#include <plasmaactivities/consumer.h>
29
30#include "activitiessync_p.h"
31#include "common/dbus/common.h"
32#include "common/specialvalues.h"
33#include "resourceslinking_interface.h"
34#include "resourcesscoring_interface.h"
35#include "utils/lazy_val.h"
36#include "utils/qsqlquery_iterator.h"
37
38#include <algorithm>
39
40#define QDBG qCDebug(PLASMA_ACTIVITIES_STATS_LOG) << "PlasmaActivitiesStats(" << (void *)this << ")"
41
42namespace KActivities
43{
44namespace Stats
45{
46// Main class
47
48class ResultWatcherPrivate
49{
50public:
51 mutable ActivitiesSync::ConsumerPtr activities;
52 QList<QRegularExpression> urlFilters;
53
54 ResultWatcherPrivate(ResultWatcher *parent, Query query)
55 : linking(KAMD_DBUS_SERVICE, QStringLiteral("/ActivityManager/Resources/Linking"), QDBusConnection::sessionBus(), nullptr)
56 , scoring(KAMD_DBUS_SERVICE, QStringLiteral("/ActivityManager/Resources/Scoring"), QDBusConnection::sessionBus(), nullptr)
57 , q(parent)
58 , query(query)
59 {
60 for (const auto &urlFilter : query.urlFilters()) {
61 urlFilters << Common::starPatternToRegex(urlFilter);
62 }
63
64 m_resultInvalidationTimer.setSingleShot(true);
65 m_resultInvalidationTimer.setInterval(200);
66 QObject::connect(&m_resultInvalidationTimer, &QTimer::timeout, q, Q_EMIT & ResultWatcher::resultsInvalidated);
67 }
68
69 template<typename Collection, typename Predicate>
70 inline bool any_of(const Collection &collection, Predicate &&predicate) const
71 {
72 const auto begin = collection.cbegin();
73 const auto end = collection.cend();
74
75 return begin == end || std::any_of(begin, end, std::forward<Predicate>(predicate));
76 }
77
78#define DEBUG_MATCHERS 0
79
80 // Processing the list of activities as specified by the query.
81 // If it contains :any, we are returning true, otherwise
82 // we want to match a specific activity (be it the current
83 // activity or not). The :global special value is not special here
84 bool activityMatches(const QString &activity) const
85 {
86#if DEBUG_MATCHERS
87 qCDebug(PLASMA_ACTIVITIES_STATS_LOG) << "Activity " << activity << "matching against" << query.activities();
88#endif
89
90 return kamd::utils::debug_and_return(DEBUG_MATCHERS,
91 " -> returning ",
92 activity == ANY_ACTIVITY_TAG || any_of(query.activities(), [&](const QString &matcher) {
93 return matcher == ANY_ACTIVITY_TAG ? true
94 : matcher == CURRENT_ACTIVITY_TAG
95 ? (matcher == activity || activity == ActivitiesSync::currentActivity(activities))
96 : activity == matcher;
97 }));
98 }
99
100 // Same as above, but for agents
101 bool agentMatches(const QString &agent) const
102 {
103#if DEBUG_MATCHERS
104 qCDebug(PLASMA_ACTIVITIES_STATS_LOG) << "Agent " << agent << "matching against" << query.agents();
105#endif
106
107 return kamd::utils::debug_and_return(DEBUG_MATCHERS, " -> returning ", agent == ANY_AGENT_TAG || any_of(query.agents(), [&](const QString &matcher) {
108 return matcher == ANY_AGENT_TAG ? true
109 : matcher == CURRENT_AGENT_TAG
110 ? (matcher == agent || agent == QCoreApplication::applicationName())
111 : agent == matcher;
112 }));
113 }
114
115 // Same as above, but for urls
116 bool urlMatches(const QString &url) const
117 {
118#if DEBUG_MATCHERS
119 qCDebug(PLASMA_ACTIVITIES_STATS_LOG) << "Url " << url << "matching against" << urlFilters;
120#endif
121
122 return kamd::utils::debug_and_return(DEBUG_MATCHERS, " -> returning ", any_of(urlFilters, [&](const QRegularExpression &matcher) {
123 return matcher.match(url).hasMatch();
124 }));
125 }
126
127 bool typeMatches(const QString &resource) const
128 {
129 // We don't necessarily need to retrieve the type from
130 // the database. If we do, get it only once
131 auto type = kamd::utils::make_lazy_val([&]() -> QString {
132 using Common::Database;
133
134 auto database = Database::instance(Database::ResourcesDatabase, Database::ReadOnly);
135
136 if (!database) {
137 return QString();
138 }
139
140 auto query = database->execQuery(QStringLiteral("SELECT mimetype FROM ResourceInfo WHERE "
141 "targettedResource = '")
142 + resource + QStringLiteral("'"));
143
144 for (const auto &item : query) {
145 return item[0].toString();
146 }
147
148 return QString();
149 });
150
151#if DEBUG_MATCHERS
152 qCDebug(PLASMA_ACTIVITIES_STATS_LOG) << "Type "
153 << "...type..."
154 << "matching against" << query.types();
155 qCDebug(PLASMA_ACTIVITIES_STATS_LOG) << "ANY_TYPE_TAG" << ANY_TYPE_TAG;
156#endif
157
158 return kamd::utils::debug_and_return(DEBUG_MATCHERS, " -> returning ", any_of(query.types(), [&](const QString &matcher) {
159 if (matcher == ANY_TYPE_TAG) {
160 return true;
161 }
162
163 const QString _type = type;
164 return matcher == ANY_TYPE_TAG
165 || (matcher == FILES_TYPE_TAG && !_type.isEmpty() && _type != QStringLiteral("inode/directory"))
166 || (matcher == DIRECTORIES_TYPE_TAG && _type == QLatin1String("inode/directory")) || matcher == type;
167 }));
168 }
169
170 bool eventMatches(const QString &agent, const QString &resource, const QString &activity) const
171 {
172 // The order of checks is not arbitrary, it is sorted
173 // from the cheapest, to the most expensive
174 return kamd::utils::debug_and_return(DEBUG_MATCHERS,
175 "event matches?",
176 agentMatches(agent) && activityMatches(activity) && urlMatches(resource) && typeMatches(resource));
177 }
178
179 void onResourceLinkedToActivity(const QString &agent, const QString &resource, const QString &activity)
180 {
181#if DEBUG_MATCHERS
182 qCDebug(PLASMA_ACTIVITIES_STATS_LOG) << "Resource has been linked: " << agent << resource << activity;
183#endif
184
185 // The used resources do not really care about the linked ones
186 if (query.selection() == Terms::UsedResources) {
187 return;
188 }
189
190 if (!eventMatches(agent, resource, activity)) {
191 return;
192 }
193
194 // TODO: See whether it makes sense to have
195 // lastUpdate/firstUpdate here as well
196 Q_EMIT q->resultLinked(resource);
197 }
198
199 void onResourceUnlinkedFromActivity(const QString &agent, const QString &resource, const QString &activity)
200 {
201#if DEBUG_MATCHERS
202 qCDebug(PLASMA_ACTIVITIES_STATS_LOG) << "Resource unlinked: " << agent << resource << activity;
203#endif
204
205 // The used resources do not really care about the linked ones
206 if (query.selection() == Terms::UsedResources) {
207 return;
208 }
209
210 if (!eventMatches(agent, resource, activity)) {
211 return;
212 }
213
214 Q_EMIT q->resultUnlinked(resource);
215 }
216
217#undef DEBUG_MATCHERS
218
219 void onResourceScoreUpdated(const QString &activity, const QString &agent, const QString &resource, double score, uint lastUpdate, uint firstUpdate)
220 {
221 Q_ASSERT_X(activity == QLatin1String("00000000-0000-0000-0000-000000000000") || !QUuid(activity).isNull(),
222 "ResultWatcher::onResourceScoreUpdated",
223 "The activity should be always specified here, no magic values");
224
225 // The linked resources do not really care about the stats
226 if (query.selection() == Terms::LinkedResources) {
227 return;
228 }
229
230 if (!eventMatches(agent, resource, activity)) {
231 return;
232 }
233
234 Q_EMIT q->resultScoreUpdated(resource, score, lastUpdate, firstUpdate);
235 }
236
237 void onEarlierStatsDeleted(QString, int)
238 {
239 // The linked resources do not really care about the stats
240 if (query.selection() == Terms::LinkedResources) {
241 return;
242 }
243
244 scheduleResultsInvalidation();
245 }
246
247 void onRecentStatsDeleted(QString, int, QString)
248 {
249 // The linked resources do not really care about the stats
250 if (query.selection() == Terms::LinkedResources) {
251 return;
252 }
253
254 scheduleResultsInvalidation();
255 }
256
257 void onStatsForResourceDeleted(const QString &activity, const QString &agent, const QString &resource)
258 {
259 if (query.selection() == Terms::LinkedResources) {
260 return;
261 }
262
263 if (activityMatches(activity) && agentMatches(agent)) {
264 if (resource.contains(QLatin1Char('*'))) {
265 scheduleResultsInvalidation();
266
267 } else if (typeMatches(resource)) {
268 if (!m_resultInvalidationTimer.isActive()) {
269 // Remove a result only if we haven't an invalidation
270 // request scheduled
271 q->resultRemoved(resource);
272 }
273 }
274 }
275 }
276
277 // Lets not send a lot of invalidation events at once
278 QTimer m_resultInvalidationTimer;
279 void scheduleResultsInvalidation()
280 {
281 QDBG << "Scheduling invalidation";
282 m_resultInvalidationTimer.start();
283 }
284
285 org::kde::ActivityManager::ResourcesLinking linking;
286 org::kde::ActivityManager::ResourcesScoring scoring;
287
288 ResultWatcher *const q;
289 Query query;
290};
291
292ResultWatcher::ResultWatcher(Query query, QObject *parent)
293 : QObject(parent)
294 , d(new ResultWatcherPrivate(this, query))
295{
296 using namespace org::kde::ActivityManager;
297 using namespace std::placeholders;
298
299 // There is no need for private slots, when we have bind
300
301 // Connecting the linking service
302 QObject::connect(&d->linking,
303 &ResourcesLinking::ResourceLinkedToActivity,
304 this,
305 std::bind(&ResultWatcherPrivate::onResourceLinkedToActivity, d, _1, _2, _3));
306 QObject::connect(&d->linking,
307 &ResourcesLinking::ResourceUnlinkedFromActivity,
308 this,
309 std::bind(&ResultWatcherPrivate::onResourceUnlinkedFromActivity, d, _1, _2, _3));
310
311 // Connecting the scoring service
312 QObject::connect(&d->scoring,
313 &ResourcesScoring::ResourceScoreUpdated,
314 this,
315 std::bind(&ResultWatcherPrivate::onResourceScoreUpdated, d, _1, _2, _3, _4, _5, _6));
316 QObject::connect(&d->scoring, &ResourcesScoring::ResourceScoreDeleted, this, std::bind(&ResultWatcherPrivate::onStatsForResourceDeleted, d, _1, _2, _3));
317 QObject::connect(&d->scoring, &ResourcesScoring::RecentStatsDeleted, this, std::bind(&ResultWatcherPrivate::onRecentStatsDeleted, d, _1, _2, _3));
318 QObject::connect(&d->scoring, &ResourcesScoring::EarlierStatsDeleted, this, std::bind(&ResultWatcherPrivate::onEarlierStatsDeleted, d, _1, _2));
319}
320
321ResultWatcher::~ResultWatcher()
322{
323 delete d;
324}
325
326void ResultWatcher::linkToActivity(const QUrl &resource, const Terms::Activity &activity, const Terms::Agent &agent)
327{
328 const auto activities = (!activity.values.isEmpty()) ? activity.values
329 : (!d->query.activities().isEmpty()) ? d->query.activities()
330 : Terms::Activity::current().values;
331 const auto agents = (!agent.values.isEmpty()) ? agent.values : (!d->query.agents().isEmpty()) ? d->query.agents() : Terms::Agent::current().values;
332
333 for (const auto &activity : activities) {
334 for (const auto &agent : agents) {
335 d->linking.LinkResourceToActivity(agent, resource.toString(), activity);
336 }
337 }
338}
339
340void ResultWatcher::unlinkFromActivity(const QUrl &resource, const Terms::Activity &activity, const Terms::Agent &agent)
341{
342 const auto activities = !activity.values.isEmpty() ? activity.values
343 : !d->query.activities().isEmpty() ? d->query.activities()
344 : Terms::Activity::current().values;
345 const auto agents = !agent.values.isEmpty() ? agent.values : !d->query.agents().isEmpty() ? d->query.agents() : Terms::Agent::current().values;
346
347 for (const auto &activity : activities) {
348 for (const auto &agent : agents) {
349 qCDebug(PLASMA_ACTIVITIES_STATS_LOG) << "Unlink " << agent << resource << activity;
350 d->linking.UnlinkResourceFromActivity(agent, resource.toString(), activity);
351 }
352 }
353}
354
355} // namespace Stats
356} // namespace KActivities
357
358#include "moc_resultwatcher.cpp"
The activities system tracks resources (documents, contacts, etc.) that the user has used.
Definition query.h:54
A very thin class that sends signals when new resources matching a predefined query are available.
void resultsInvalidated()
Emitted when the client should forget about all the results it knew about and reload them.
KSERVICE_EXPORT KService::List query(FilterFunc filterFunc)
VehicleSection::Type type(QStringView coachNumber, QStringView coachClassification)
const QList< QKeySequence > & begin()
const QList< QKeySequence > & end()
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
QRegularExpressionMatch match(QStringView subjectView, qsizetype offset, MatchType matchType, MatchOptions matchOptions) const const
bool hasMatch() const const
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
void start()
void timeout()
QString toString(FormattingOptions options) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Feb 28 2025 12:01:02 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.