File indexing completed on 2025-01-26 05:00:55

0001 /*
0002  *   SPDX-FileCopyrightText: 2011, 2012, 2013, 2014 Ivan Cukic <ivan.cukic(at)kde.org>
0003  *
0004  *   SPDX-License-Identifier: GPL-2.0-or-later
0005  */
0006 
0007 // Self
0008 #include "StatsPlugin.h"
0009 #include <kactivities-features.h>
0010 
0011 // Qt
0012 #include <QDBusConnection>
0013 #include <QFileSystemWatcher>
0014 #include <QStringList>
0015 
0016 // KDE
0017 #include <kconfig.h>
0018 #include <kfileitem.h>
0019 
0020 // Boost
0021 #include <boost/range/algorithm/binary_search.hpp>
0022 #include <utils/range.h>
0023 
0024 // Local
0025 #include "../../Event.h"
0026 #include "Database.h"
0027 #include "ResourceLinking.h"
0028 #include "ResourceScoreMaintainer.h"
0029 #include "Utils.h"
0030 #include "common/specialvalues.h"
0031 #include "resourcescoringadaptor.h"
0032 
0033 K_PLUGIN_CLASS(StatsPlugin)
0034 
0035 StatsPlugin *StatsPlugin::s_instance = nullptr;
0036 
0037 StatsPlugin::StatsPlugin(QObject *parent)
0038     : Plugin(parent)
0039     , m_activities(nullptr)
0040     , m_resources(nullptr)
0041     , m_resourceLinking(new ResourceLinking(this))
0042 {
0043     s_instance = this;
0044 
0045     new ResourcesScoringAdaptor(this);
0046     QDBusConnection::sessionBus().registerObject(QStringLiteral("/ActivityManager/Resources/Scoring"), this);
0047 
0048     setName(QStringLiteral("org.kde.ActivityManager.Resources.Scoring"));
0049 }
0050 
0051 bool StatsPlugin::init(QHash<QString, QObject *> &modules)
0052 {
0053     Plugin::init(modules);
0054 
0055     if (!resourcesDatabase()) {
0056         return false;
0057     }
0058 
0059     m_activities = modules[QStringLiteral("activities")];
0060     m_resources = modules[QStringLiteral("resources")];
0061 
0062     m_resourceLinking->init();
0063 
0064     connect(m_resources, SIGNAL(ProcessedResourceEvents(EventList)), this, SLOT(addEvents(EventList)));
0065     connect(m_resources, SIGNAL(RegisteredResourceMimetype(QString, QString)), this, SLOT(saveResourceMimetype(QString, QString)));
0066     connect(m_resources, SIGNAL(RegisteredResourceTitle(QString, QString)), this, SLOT(saveResourceTitle(QString, QString)));
0067 
0068     connect(modules[QStringLiteral("config")], SIGNAL(pluginConfigChanged()), this, SLOT(loadConfiguration()));
0069 
0070     loadConfiguration();
0071 
0072     return true;
0073 }
0074 
0075 void StatsPlugin::loadConfiguration()
0076 {
0077     auto conf = config();
0078     conf.config()->reparseConfiguration();
0079 
0080     m_blockedByDefault = conf.readEntry("blocked-by-default", false);
0081     m_blockAll = false;
0082     m_whatToRemember = (WhatToRemember)conf.readEntry("what-to-remember", (int)AllApplications);
0083 
0084     m_apps.clear();
0085 
0086     if (m_whatToRemember == SpecificApplications) {
0087         auto apps = conf.readEntry(m_blockedByDefault ? "allowed-applications" : "blocked-applications", QStringList());
0088 
0089         m_apps.unite({apps.begin(), apps.end()});
0090     }
0091 
0092     // Delete old events, as per configuration.
0093     // For people who do not restart their computers, we should do this from
0094     // time to time. Doing this twice a day should be more than enough.
0095     deleteOldEvents();
0096     m_deleteOldEventsTimer.setInterval(12 * 60 * 60 * 1000);
0097     connect(&m_deleteOldEventsTimer, &QTimer::timeout, this, &StatsPlugin::deleteOldEvents);
0098 
0099     // Loading URL filters
0100     m_urlFilters.clear();
0101 
0102     auto filters = conf.readEntry("url-filters",
0103                                   QStringList{"about:*", // Ignore about: stuff
0104                                               "*/.*", // Ignore hidden files
0105                                               "/", // Ignore root
0106                                               "/tmp/*"} // Ignore everything in /tmp
0107     );
0108 
0109     for (const auto &filter : filters) {
0110         m_urlFilters << Common::starPatternToRegex(filter);
0111     }
0112 
0113     // Loading the private activities
0114     m_otrActivities = conf.readEntry("off-the-record-activities", QStringList());
0115 }
0116 
0117 void StatsPlugin::deleteOldEvents()
0118 {
0119     DeleteEarlierStats(QString(), config().readEntry("keep-history-for", 0));
0120 }
0121 
0122 void StatsPlugin::openResourceEvent(const QString &usedActivity,
0123                                     const QString &initiatingAgent,
0124                                     const QString &targettedResource,
0125                                     const QDateTime &start,
0126                                     const QDateTime &end)
0127 {
0128     Q_ASSERT_X(!initiatingAgent.isEmpty(), "StatsPlugin::openResourceEvent", "Agent should not be empty");
0129     Q_ASSERT_X(!usedActivity.isEmpty(), "StatsPlugin::openResourceEvent", "Activity should not be empty");
0130     Q_ASSERT_X(!targettedResource.isEmpty(), "StatsPlugin::openResourceEvent", "Resource should not be empty");
0131 
0132     detectResourceInfo(targettedResource);
0133 
0134     Utils::prepare(*resourcesDatabase(),
0135                    openResourceEventQuery,
0136                    QStringLiteral("INSERT INTO ResourceEvent"
0137                                   "        (usedActivity,  initiatingAgent,  targettedResource,  start,  end) "
0138                                   "VALUES (:usedActivity, :initiatingAgent, :targettedResource, :start, :end)"));
0139 
0140     Utils::exec(*resourcesDatabase(),
0141                 Utils::FailOnError,
0142                 *openResourceEventQuery,
0143                 ":usedActivity",
0144                 usedActivity,
0145                 ":initiatingAgent",
0146                 initiatingAgent,
0147                 ":targettedResource",
0148                 targettedResource,
0149                 ":start",
0150                 start.toSecsSinceEpoch(),
0151                 ":end",
0152                 (end.isNull()) ? QVariant() : end.toSecsSinceEpoch());
0153 }
0154 
0155 void StatsPlugin::closeResourceEvent(const QString &usedActivity, const QString &initiatingAgent, const QString &targettedResource, const QDateTime &end)
0156 {
0157     Q_ASSERT_X(!initiatingAgent.isEmpty(), "StatsPlugin::closeResourceEvent", "Agent should not be empty");
0158     Q_ASSERT_X(!usedActivity.isEmpty(), "StatsPlugin::closeResourceEvent", "Activity should not be empty");
0159     Q_ASSERT_X(!targettedResource.isEmpty(), "StatsPlugin::closeResourceEvent", "Resource should not be empty");
0160 
0161     Utils::prepare(*resourcesDatabase(),
0162                    closeResourceEventQuery,
0163                    QStringLiteral("UPDATE ResourceEvent "
0164                                   "SET end = :end "
0165                                   "WHERE "
0166                                   ":usedActivity      = usedActivity AND "
0167                                   ":initiatingAgent   = initiatingAgent AND "
0168                                   ":targettedResource = targettedResource AND "
0169                                   "end IS NULL"));
0170 
0171     Utils::exec(*resourcesDatabase(),
0172                 Utils::FailOnError,
0173                 *closeResourceEventQuery,
0174                 ":usedActivity",
0175                 usedActivity,
0176                 ":initiatingAgent",
0177                 initiatingAgent,
0178                 ":targettedResource",
0179                 targettedResource,
0180                 ":end",
0181                 end.toSecsSinceEpoch());
0182 }
0183 
0184 void StatsPlugin::detectResourceInfo(const QString &_uri)
0185 {
0186     const QUrl uri = QUrl::fromUserInput(_uri);
0187 
0188     if (!uri.isLocalFile())
0189         return;
0190 
0191     const QString file = uri.toLocalFile();
0192 
0193     if (!QFile::exists(file))
0194         return;
0195 
0196     KFileItem item(uri);
0197 
0198     if (insertResourceInfo(file)) {
0199         saveResourceMimetype(file, item.mimetype(), true);
0200 
0201         const auto text = item.text();
0202         saveResourceTitle(file, text.isEmpty() ? _uri : text, true);
0203     }
0204 }
0205 
0206 bool StatsPlugin::insertResourceInfo(const QString &uri)
0207 {
0208     Utils::prepare(*resourcesDatabase(),
0209                    getResourceInfoQuery,
0210                    QStringLiteral("SELECT targettedResource FROM ResourceInfo WHERE "
0211                                   "  targettedResource = :targettedResource "));
0212 
0213     getResourceInfoQuery->bindValue(":targettedResource", uri);
0214     Utils::exec(*resourcesDatabase(), Utils::FailOnError, *getResourceInfoQuery);
0215 
0216     if (getResourceInfoQuery->next()) {
0217         return false;
0218     }
0219 
0220     Utils::prepare(*resourcesDatabase(),
0221                    insertResourceInfoQuery,
0222                    QStringLiteral("INSERT INTO ResourceInfo( "
0223                                   "  targettedResource"
0224                                   ", title"
0225                                   ", autoTitle"
0226                                   ", mimetype"
0227                                   ", autoMimetype"
0228                                   ") VALUES ("
0229                                   "  :targettedResource"
0230                                   ", '' "
0231                                   ", 1 "
0232                                   ", '' "
0233                                   ", 1 "
0234                                   ")"));
0235 
0236     Utils::exec(*resourcesDatabase(), Utils::FailOnError, *insertResourceInfoQuery, ":targettedResource", uri);
0237 
0238     return true;
0239 }
0240 
0241 void StatsPlugin::saveResourceTitle(const QString &uri, const QString &title, bool autoTitle)
0242 {
0243     if (m_blockAll || m_whatToRemember == NoApplications) {
0244         return;
0245     }
0246 
0247     insertResourceInfo(uri);
0248 
0249     DATABASE_TRANSACTION(*resourcesDatabase());
0250 
0251     Utils::prepare(*resourcesDatabase(),
0252                    saveResourceTitleQuery,
0253                    QStringLiteral("UPDATE ResourceInfo SET "
0254                                   "  title = :title"
0255                                   ", autoTitle = :autoTitle "
0256                                   "WHERE "
0257                                   "targettedResource = :targettedResource "));
0258 
0259     Utils::exec(*resourcesDatabase(),
0260                 Utils::FailOnError,
0261                 *saveResourceTitleQuery,
0262                 ":targettedResource",
0263                 uri,
0264                 ":title",
0265                 title,
0266                 ":autoTitle",
0267                 (autoTitle ? "1" : "0"));
0268 }
0269 
0270 void StatsPlugin::saveResourceMimetype(const QString &uri, const QString &mimetype, bool autoMimetype)
0271 {
0272     if (m_blockAll || m_whatToRemember == NoApplications) {
0273         return;
0274     }
0275 
0276     insertResourceInfo(uri);
0277 
0278     DATABASE_TRANSACTION(*resourcesDatabase());
0279 
0280     Utils::prepare(*resourcesDatabase(),
0281                    saveResourceMimetypeQuery,
0282                    QStringLiteral("UPDATE ResourceInfo SET "
0283                                   "  mimetype = :mimetype"
0284                                   ", autoMimetype = :autoMimetype "
0285                                   "WHERE "
0286                                   "targettedResource = :targettedResource "));
0287 
0288     Utils::exec(*resourcesDatabase(),
0289                 Utils::FailOnError,
0290                 *saveResourceMimetypeQuery,
0291                 ":targettedResource",
0292                 uri,
0293                 ":mimetype",
0294                 mimetype,
0295                 ":autoMimetype",
0296                 (autoMimetype ? "1" : "0"));
0297 }
0298 
0299 StatsPlugin *StatsPlugin::self()
0300 {
0301     return s_instance;
0302 }
0303 
0304 bool StatsPlugin::acceptedEvent(const Event &event)
0305 {
0306     using std::any_of;
0307     using std::bind;
0308     using namespace std::placeholders;
0309 
0310     return !(
0311         // If the URI is empty, we do not want to process it
0312         event.uri.isEmpty() ||
0313 
0314         // Skip if the current activity is OTR
0315         m_otrActivities.contains(currentActivity()) ||
0316 
0317         // Exclude URIs that match the ignored patterns
0318         any_of(m_urlFilters.cbegin(), m_urlFilters.cend(), [event] (const QRegularExpression &regex){ return regex.match(event.uri).hasMatch(); }) ||
0319 
0320         // if blocked by default, the list contains allowed applications
0321         //     ignore event if the list doesn't contain the application
0322         // if not blocked by default, the list contains blocked applications
0323         //     ignore event if the list contains the application
0324         (m_whatToRemember == SpecificApplications && m_blockedByDefault != boost::binary_search(m_apps, event.application)));
0325 }
0326 
0327 Event StatsPlugin::validateEvent(Event event)
0328 {
0329     if (event.uri.startsWith(QStringLiteral("file://"))) {
0330         event.uri = QUrl(event.uri).toLocalFile();
0331     }
0332 
0333     if (event.uri.startsWith(QStringLiteral("/"))) {
0334         QFileInfo file(event.uri);
0335 
0336         event.uri = file.exists() ? file.canonicalFilePath() : QString();
0337     }
0338 
0339     return event;
0340 }
0341 
0342 QStringList StatsPlugin::listActivities() const
0343 {
0344     return Plugin::retrieve<QStringList>(m_activities, "ListActivities");
0345 }
0346 
0347 QString StatsPlugin::currentActivity() const
0348 {
0349     return Plugin::retrieve<QString>(m_activities, "CurrentActivity");
0350 }
0351 
0352 void StatsPlugin::addEvents(const EventList &events)
0353 {
0354     using namespace kamd::utils;
0355 
0356     if (m_blockAll || m_whatToRemember == NoApplications) {
0357         return;
0358     }
0359 
0360     const auto &eventsToProcess = events | transformed(&StatsPlugin::validateEvent, this) | filtered(&StatsPlugin::acceptedEvent, this);
0361 
0362     if (eventsToProcess.begin() == eventsToProcess.end())
0363         return;
0364 
0365     DATABASE_TRANSACTION(*resourcesDatabase());
0366 
0367     for (const auto &event : eventsToProcess) {
0368         switch (event.type) {
0369         case Event::Accessed:
0370             openResourceEvent(currentActivity(), event.application, event.uri, event.timestamp, event.timestamp);
0371             ResourceScoreMaintainer::self()->processResource(event.uri, event.application);
0372 
0373             break;
0374 
0375         case Event::Opened:
0376             openResourceEvent(currentActivity(), event.application, event.uri, event.timestamp);
0377 
0378             break;
0379 
0380         case Event::Closed:
0381             closeResourceEvent(currentActivity(), event.application, event.uri, event.timestamp);
0382             ResourceScoreMaintainer::self()->processResource(event.uri, event.application);
0383 
0384             break;
0385 
0386         case Event::UserEventType:
0387             ResourceScoreMaintainer::self()->processResource(event.uri, event.application);
0388             break;
0389 
0390         default:
0391             // Nothing yet
0392             // TODO: Add focus and modification
0393             break;
0394         }
0395     }
0396 }
0397 
0398 void StatsPlugin::DeleteRecentStats(const QString &activity, int count, const QString &what)
0399 {
0400     const auto usedActivity = activity.isEmpty() ? QVariant() : QVariant(activity);
0401 
0402     // If we need to delete everything,
0403     // no need to bother with the count and the date
0404 
0405     DATABASE_TRANSACTION(*resourcesDatabase());
0406 
0407     if (what == QStringLiteral("everything")) {
0408         // Instantiating these every time is not a big overhead
0409         // since this method is rarely executed.
0410 
0411         auto removeResourceInfoQuery = resourcesDatabase()->createQuery();
0412         removeResourceInfoQuery.prepare(
0413             "DELETE FROM ResourceInfo "
0414             "WHERE ResourceInfo.targettedResource IN ("
0415             "   SELECT ResourceEvent.targettedResource "
0416             "   FROM ResourceEvent "
0417             "   WHERE usedActivity = COALESCE(:usedActivity, usedActivity)"
0418             ")");
0419 
0420         auto removeEventsQuery = resourcesDatabase()->createQuery();
0421         removeEventsQuery.prepare(
0422             "DELETE FROM ResourceEvent "
0423             "WHERE usedActivity = COALESCE(:usedActivity, usedActivity)");
0424 
0425         auto removeScoreCachesQuery = resourcesDatabase()->createQuery();
0426         removeScoreCachesQuery.prepare(
0427             "DELETE FROM ResourceScoreCache "
0428             "WHERE usedActivity = COALESCE(:usedActivity, usedActivity)");
0429 
0430         Utils::exec(*resourcesDatabase(), Utils::FailOnError, removeResourceInfoQuery, ":usedActivity", usedActivity);
0431         Utils::exec(*resourcesDatabase(), Utils::FailOnError, removeEventsQuery, ":usedActivity", usedActivity);
0432         Utils::exec(*resourcesDatabase(), Utils::FailOnError, removeScoreCachesQuery, ":usedActivity", usedActivity);
0433 
0434     } else {
0435         // Deleting a specified length of time
0436 
0437         auto since = QDateTime::currentDateTime();
0438 
0439         since = (what[0] == QLatin1Char('h')) ? since.addSecs(-count * 60 * 60)
0440             : (what[0] == QLatin1Char('d'))   ? since.addDays(-count)
0441             : (what[0] == QLatin1Char('m'))   ? since.addMonths(-count)
0442                                               : since;
0443 
0444         // Maybe we should decrease the scores for the previously
0445         // cached items. Thinking it is not that important -
0446         // if something was accessed before, and the user did not
0447         // remove the history, it is not really a secret.
0448 
0449         auto removeResourceInfoQuery = resourcesDatabase()->createQuery();
0450         removeResourceInfoQuery.prepare(
0451             "DELETE FROM ResourceInfo "
0452             "WHERE ResourceInfo.targettedResource IN ("
0453             "   SELECT ResourceEvent.targettedResource "
0454             "   FROM ResourceEvent "
0455             "   WHERE usedActivity = COALESCE(:usedActivity, usedActivity) "
0456             "   AND end > :since"
0457             ")");
0458 
0459         auto removeEventsQuery = resourcesDatabase()->createQuery();
0460         removeEventsQuery.prepare(
0461             "DELETE FROM ResourceEvent "
0462             "WHERE usedActivity = COALESCE(:usedActivity, usedActivity) "
0463             "AND end > :since");
0464 
0465         auto removeScoreCachesQuery = resourcesDatabase()->createQuery();
0466         removeScoreCachesQuery.prepare(
0467             "DELETE FROM ResourceScoreCache "
0468             "WHERE usedActivity = COALESCE(:usedActivity, usedActivity) "
0469             "AND firstUpdate > :since");
0470 
0471         Utils::exec(*resourcesDatabase(), Utils::FailOnError, removeResourceInfoQuery, ":usedActivity", usedActivity, ":since", since.toSecsSinceEpoch());
0472 
0473         Utils::exec(*resourcesDatabase(), Utils::FailOnError, removeEventsQuery, ":usedActivity", usedActivity, ":since", since.toSecsSinceEpoch());
0474 
0475         Utils::exec(*resourcesDatabase(), Utils::FailOnError, removeScoreCachesQuery, ":usedActivity", usedActivity, ":since", since.toSecsSinceEpoch());
0476     }
0477 
0478     Q_EMIT RecentStatsDeleted(activity, count, what);
0479 }
0480 
0481 void StatsPlugin::DeleteEarlierStats(const QString &activity, int months)
0482 {
0483     if (months == 0) {
0484         return;
0485     }
0486 
0487     // Deleting a specified length of time
0488 
0489     DATABASE_TRANSACTION(*resourcesDatabase());
0490 
0491     const auto time = QDateTime::currentDateTime().addMonths(-months);
0492     const auto usedActivity = activity.isEmpty() ? QVariant() : QVariant(activity);
0493 
0494     auto removeResourceInfoQuery = resourcesDatabase()->createQuery();
0495     removeResourceInfoQuery.prepare(
0496         "DELETE FROM ResourceInfo "
0497         "WHERE ResourceInfo.targettedResource IN ("
0498         "   SELECT ResourceEvent.targettedResource "
0499         "   FROM ResourceEvent "
0500         "   WHERE usedActivity = COALESCE(:usedActivity, usedActivity) "
0501         "   AND start < :time"
0502         ")");
0503 
0504     auto removeEventsQuery = resourcesDatabase()->createQuery();
0505     removeEventsQuery.prepare(
0506         "DELETE FROM ResourceEvent "
0507         "WHERE usedActivity = COALESCE(:usedActivity, usedActivity) "
0508         "AND start < :time");
0509 
0510     auto removeScoreCachesQuery = resourcesDatabase()->createQuery();
0511     removeScoreCachesQuery.prepare(
0512         "DELETE FROM ResourceScoreCache "
0513         "WHERE usedActivity = COALESCE(:usedActivity, usedActivity) "
0514         "AND lastUpdate < :time");
0515 
0516     Utils::exec(*resourcesDatabase(), Utils::FailOnError, removeResourceInfoQuery, ":usedActivity", usedActivity, ":time", time.toSecsSinceEpoch());
0517 
0518     Utils::exec(*resourcesDatabase(), Utils::FailOnError, removeEventsQuery, ":usedActivity", usedActivity, ":time", time.toSecsSinceEpoch());
0519 
0520     Utils::exec(*resourcesDatabase(), Utils::FailOnError, removeScoreCachesQuery, ":usedActivity", usedActivity, ":time", time.toSecsSinceEpoch());
0521 
0522     Q_EMIT EarlierStatsDeleted(activity, months);
0523 }
0524 
0525 void StatsPlugin::DeleteStatsForResource(const QString &activity, const QString &client, const QString &resource)
0526 {
0527     Q_ASSERT_X(!client.isEmpty(), "StatsPlugin::DeleteStatsForResource", "Agent should not be empty");
0528     Q_ASSERT_X(!activity.isEmpty(), "StatsPlugin::DeleteStatsForResource", "Activity should not be empty");
0529     Q_ASSERT_X(!resource.isEmpty(), "StatsPlugin::DeleteStatsForResource", "Resource should not be empty");
0530     Q_ASSERT_X(client != CURRENT_AGENT_TAG, "StatsPlugin::DeleteStatsForResource", "We can not handle CURRENT_AGENT_TAG here");
0531 
0532     DATABASE_TRANSACTION(*resourcesDatabase());
0533 
0534     // Check against sql injection
0535     if (activity.contains('\'') || client.contains('\''))
0536         return;
0537 
0538     const auto activityFilter =
0539         activity == ANY_ACTIVITY_TAG ? " 1 " : QStringLiteral(" usedActivity = '%1' ").arg(activity == CURRENT_ACTIVITY_TAG ? currentActivity() : activity);
0540 
0541     const auto clientFilter = client == ANY_AGENT_TAG ? " 1 " : QStringLiteral(" initiatingAgent = '%1' ").arg(client);
0542 
0543     auto removeResourceInfoQuery = resourcesDatabase()->createQuery();
0544     removeResourceInfoQuery.prepare(
0545         "DELETE FROM ResourceInfo "
0546         "WHERE targettedResource LIKE :targettedResource ESCAPE '\\'");
0547 
0548     auto removeEventsQuery = resourcesDatabase()->createQuery();
0549     removeEventsQuery.prepare(
0550         "DELETE FROM ResourceEvent "
0551         "WHERE "
0552         + activityFilter + " AND " + clientFilter + " AND " + "targettedResource LIKE :targettedResource ESCAPE '\\'");
0553 
0554     auto removeScoreCachesQuery = resourcesDatabase()->createQuery();
0555     removeScoreCachesQuery.prepare(
0556         "DELETE FROM ResourceScoreCache "
0557         "WHERE "
0558         + activityFilter + " AND " + clientFilter + " AND " + "targettedResource LIKE :targettedResource ESCAPE '\\'");
0559 
0560     const auto pattern = Common::starPatternToLike(resource);
0561 
0562     Utils::exec(*resourcesDatabase(), Utils::FailOnError, removeResourceInfoQuery, ":targettedResource", pattern);
0563 
0564     Utils::exec(*resourcesDatabase(), Utils::FailOnError, removeEventsQuery, ":targettedResource", pattern);
0565 
0566     Utils::exec(*resourcesDatabase(), Utils::FailOnError, removeScoreCachesQuery, ":targettedResource", pattern);
0567 
0568     Q_EMIT ResourceScoreDeleted(activity, client, resource);
0569 }
0570 
0571 bool StatsPlugin::isFeatureOperational(const QStringList &feature) const
0572 {
0573     if (feature[0] == "isOTR") {
0574         if (feature.size() != 2)
0575             return true;
0576 
0577         const auto activity = feature[1];
0578 
0579         return activity == "activity" //
0580             || activity == "current" //
0581             || listActivities().contains(activity);
0582 
0583         return true;
0584     }
0585 
0586     return false;
0587 }
0588 
0589 // bool StatsPlugin::isFeatureEnabled(const QStringList &feature) const
0590 // {
0591 //     if (feature[0] == "isOTR") {
0592 //         if (feature.size() != 2) return false;
0593 //
0594 //         auto activity = feature[1];
0595 //
0596 //         if (activity == "activity" || activity == "current") {
0597 //             activity = currentActivity();
0598 //         }
0599 //
0600 //         return m_otrActivities.contains(activity);
0601 //     }
0602 //
0603 //     return false;
0604 // }
0605 //
0606 // void StatsPlugin::setFeatureEnabled(const QStringList &feature, bool value)
0607 // {
0608 //     if (feature[0] == "isOTR") {
0609 //         if (feature.size() != 2) return;
0610 //
0611 //         auto activity = feature[1];
0612 //
0613 //         if (activity == "activity" || activity == "current") {
0614 //             activity = currentActivity();
0615 //         }
0616 //
0617 //         if (!m_otrActivities.contains(activity)) {
0618 //             m_otrActivities << activity;
0619 //             config().writeEntry("off-the-record-activities", m_otrActivities);
0620 //             config().sync();
0621 //         }
0622 //     }
0623 // }
0624 
0625 QDBusVariant StatsPlugin::featureValue(const QStringList &feature) const
0626 {
0627     if (feature[0] == "isOTR") {
0628         if (feature.size() != 2)
0629             return QDBusVariant(false);
0630 
0631         auto activity = feature[1];
0632 
0633         if (activity == "activity" || activity == "current") {
0634             activity = currentActivity();
0635         }
0636 
0637         return QDBusVariant(m_otrActivities.contains(activity));
0638     }
0639 
0640     return QDBusVariant(false);
0641 }
0642 
0643 void StatsPlugin::setFeatureValue(const QStringList &feature, const QDBusVariant &value)
0644 {
0645     if (feature[0] == "isOTR") {
0646         if (feature.size() != 2)
0647             return;
0648 
0649         auto activity = feature[1];
0650 
0651         if (activity == "activity" || activity == "current") {
0652             activity = currentActivity();
0653         }
0654 
0655         bool isOTR = value.variant().toBool();
0656 
0657         if (isOTR && !m_otrActivities.contains(activity)) {
0658             m_otrActivities << activity;
0659 
0660         } else if (!isOTR && m_otrActivities.contains(activity)) {
0661             m_otrActivities.removeAll(activity);
0662         }
0663 
0664         config().writeEntry("off-the-record-activities", m_otrActivities);
0665         config().sync();
0666     }
0667 }
0668 
0669 QStringList StatsPlugin::listFeatures(const QStringList &feature) const
0670 {
0671     if (feature.isEmpty() || feature[0].isEmpty()) {
0672         return {"isOTR/"};
0673 
0674     } else if (feature[0] == "isOTR") {
0675         return listActivities();
0676     }
0677 
0678     return QStringList();
0679 }
0680 
0681 #include "StatsPlugin.moc"
0682 
0683 #include "moc_StatsPlugin.cpp"