File indexing completed on 2022-12-06 13:15:25

0001 /*
0002     SPDX-FileCopyrightText: 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 "resourcemodel.h"
0009 
0010 // Qt
0011 #include <QByteArray>
0012 #include <QCoreApplication>
0013 #include <QDebug>
0014 #include <QModelIndex>
0015 #include <QSqlQuery>
0016 #include <QUuid>
0017 
0018 // KDE
0019 #include <KConfig>
0020 #include <KDesktopFile>
0021 #include <KFileItem>
0022 #include <ksharedconfig.h>
0023 
0024 // STL and Boost
0025 #include <boost/algorithm/string/join.hpp>
0026 #include <boost/range/adaptor/filtered.hpp>
0027 #include <boost/range/adaptor/transformed.hpp>
0028 #include <boost/range/algorithm/find_if.hpp>
0029 #include <boost/range/numeric.hpp>
0030 #include <mutex>
0031 
0032 // Local
0033 #include "common/dbus/common.h"
0034 #include "utils/dbusfuture_p.h"
0035 #include "utils/range.h"
0036 
0037 #define ENABLE_QJSVALUE_CONTINUATION
0038 #include "utils/continue_with.h"
0039 
0040 #define ACTIVITY_COLUMN 0
0041 #define AGENT_COLUMN 1
0042 #define RESOURCE_COLUMN 2
0043 #define UNKNOWN_COLUMN 3
0044 
0045 using kamd::utils::continue_with;
0046 
0047 namespace KActivities
0048 {
0049 namespace Imports
0050 {
0051 class ResourceModel::LinkerService : public QDBusInterface
0052 {
0053 private:
0054     LinkerService()
0055         : KAMD_DBUS_INTERFACE("Resources/Linking", ResourcesLinking, nullptr)
0056     {
0057     }
0058 
0059 public:
0060     static std::shared_ptr<LinkerService> self()
0061     {
0062         static std::weak_ptr<LinkerService> s_instance;
0063         static std::mutex singleton;
0064 
0065         std::lock_guard<std::mutex> singleton_lock(singleton);
0066 
0067         auto result = s_instance.lock();
0068 
0069         if (s_instance.expired()) {
0070             result.reset(new LinkerService());
0071             s_instance = result;
0072         }
0073 
0074         return result;
0075     }
0076 };
0077 
0078 ResourceModel::ResourceModel(QObject *parent)
0079     : QSortFilterProxyModel(parent)
0080     , m_shownActivities(QStringLiteral(":current"))
0081     , m_shownAgents(QStringLiteral(":current"))
0082     , m_defaultItemsLoaded(false)
0083     , m_linker(LinkerService::self())
0084     , m_config(KSharedConfig::openConfig("kactivitymanagerd-resourcelinkingrc")->group("Order"))
0085 {
0086     // NOTE: What to do if the file does not exist?
0087     //       Ignoring that case since the daemon creates it on startup.
0088     //       Is it plausible that somebody will instantiate the ResourceModel
0089     //       before the daemon is started?
0090 
0091     const QString databaseDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/kactivitymanagerd/resources/");
0092 
0093     m_databaseFile = databaseDir + QStringLiteral("database");
0094 
0095     loadDatabase();
0096 
0097     connect(&m_service, &KActivities::Consumer::currentActivityChanged, this, &ResourceModel::onCurrentActivityChanged);
0098 
0099     connect(m_linker.get(), SIGNAL(ResourceLinkedToActivity(QString, QString, QString)), this, SLOT(onResourceLinkedToActivity(QString, QString, QString)));
0100     connect(m_linker.get(),
0101             SIGNAL(ResourceUnlinkedFromActivity(QString, QString, QString)),
0102             this,
0103             SLOT(onResourceUnlinkedFromActivity(QString, QString, QString)));
0104 
0105     setDynamicSortFilter(true);
0106     sort(0);
0107 }
0108 
0109 bool ResourceModel::loadDatabase()
0110 {
0111     if (m_database.isValid())
0112         return true;
0113     if (!QFile(m_databaseFile).exists())
0114         return false;
0115 
0116     // TODO: Database connection naming could be smarter (thread-id-based,
0117     //       reusing connections...?)
0118     m_database = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), QStringLiteral("kactivities_db_resources_") + QString::number((quintptr)this));
0119 
0120     // qDebug() << "Database file is: " << m_databaseFile;
0121     m_database.setDatabaseName(m_databaseFile);
0122 
0123     m_database.open();
0124 
0125     m_databaseModel = new QSqlTableModel(this, m_database);
0126     m_databaseModel->setTable("ResourceLink");
0127     m_databaseModel->select();
0128 
0129     setSourceModel(m_databaseModel);
0130 
0131     reloadData();
0132 
0133     return true;
0134 }
0135 
0136 ResourceModel::~ResourceModel()
0137 {
0138 }
0139 
0140 QVariant ResourceModel::dataForColumn(const QModelIndex &index, int column) const
0141 {
0142     if (!m_database.isValid())
0143         return QVariant();
0144 
0145     return m_databaseModel->data(index.sibling(index.row(), column), Qt::DisplayRole);
0146 }
0147 
0148 bool ResourceModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
0149 {
0150     const auto leftResource = dataForColumn(left, RESOURCE_COLUMN).toString();
0151     const auto rightResource = dataForColumn(right, RESOURCE_COLUMN).toString();
0152 
0153     const bool hasLeft = m_sorting.contains(leftResource);
0154     const bool hasRight = m_sorting.contains(rightResource);
0155 
0156     return (hasLeft && !hasRight) ? true
0157         : (!hasLeft && hasRight)  ? false
0158         : (hasLeft && hasRight)   ? m_sorting.indexOf(leftResource) < m_sorting.indexOf(rightResource)
0159                                   : QString::compare(leftResource, rightResource, Qt::CaseInsensitive) < 0;
0160 }
0161 
0162 QHash<int, QByteArray> ResourceModel::roleNames() const
0163 {
0164     return {{Qt::DisplayRole, "display"},
0165             {Qt::DecorationRole, "decoration"},
0166             {ResourceRole, "uri"},
0167             {AgentRole, "agent"},
0168             {ActivityRole, "activity"},
0169             {DescriptionRole, "subtitle"}};
0170 }
0171 
0172 template<typename Validator>
0173 inline QStringList validateList(const QString &values, Validator validator)
0174 {
0175     using boost::adaptors::filtered;
0176     using kamd::utils::as_collection;
0177 
0178     auto result = as_collection<QStringList>(values.split(',') | filtered(validator));
0179 
0180     if (result.isEmpty()) {
0181         result.append(QStringLiteral(":current"));
0182     }
0183 
0184     return result;
0185 }
0186 
0187 void ResourceModel::setShownActivities(const QString &activities)
0188 {
0189     m_shownActivities = validateList(activities, [&](const QString &activity) {
0190         return activity == ":current" || activity == ":any" || activity == ":global" || !QUuid(activity).isNull();
0191     });
0192 
0193     reloadData();
0194     Q_EMIT shownActivitiesChanged();
0195 }
0196 
0197 void ResourceModel::setShownAgents(const QString &agents)
0198 {
0199     m_shownAgents = validateList(agents, [&](const QString &agent) {
0200         return agent == ":current" || agent == ":any" || agent == ":global" || (!agent.isEmpty() && !agent.contains('\'') && !agent.contains('"'));
0201     });
0202 
0203     loadDefaultsIfNeeded();
0204     reloadData();
0205     Q_EMIT shownAgentsChanged();
0206 }
0207 
0208 QString ResourceModel::shownActivities() const
0209 {
0210     return m_shownActivities.join(',');
0211 }
0212 
0213 QString ResourceModel::shownAgents() const
0214 {
0215     return m_shownAgents.join(',');
0216 }
0217 
0218 QString ResourceModel::defaultItemsConfig() const
0219 {
0220     return m_defaultItemsConfig;
0221 }
0222 
0223 void ResourceModel::setDefaultItemsConfig(const QString &defaultItemsConfig)
0224 {
0225     m_defaultItemsConfig = defaultItemsConfig;
0226     loadDefaultsIfNeeded();
0227 }
0228 
0229 QString ResourceModel::activityToWhereClause(const QString &shownActivity) const
0230 {
0231     return QStringLiteral(" OR usedActivity=")
0232         + (shownActivity == ":current"      ? "'" + m_service.currentActivity() + "'"
0233                : shownActivity == ":any"    ? "usedActivity"
0234                : shownActivity == ":global" ? "''"
0235                                             : "'" + shownActivity + "'");
0236 }
0237 
0238 QString ResourceModel::agentToWhereClause(const QString &shownAgent) const
0239 {
0240     return QStringLiteral(" OR initiatingAgent=")
0241         + (shownAgent == ":current"      ? "'" + QCoreApplication::applicationName() + "'"
0242                : shownAgent == ":any"    ? "initiatingAgent"
0243                : shownAgent == ":global" ? "''"
0244                                          : "'" + shownAgent + "'");
0245 }
0246 
0247 QString ResourceModel::whereClause(const QStringList &activities, const QStringList &agents) const
0248 {
0249     using boost::accumulate;
0250     using namespace kamd::utils;
0251 
0252     // qDebug() << "Getting the where clause for: " << activities << " " << agents;
0253 
0254     // Defining the transformation functions for generating the SQL WHERE clause
0255     // from the specified activity/agent. They also resolve the special values
0256     // like :current, :any and :global.
0257 
0258     auto activityToWhereClause = transformed(&ResourceModel::activityToWhereClause, this);
0259     auto agentToWhereClause = transformed(&ResourceModel::agentToWhereClause, this);
0260 
0261     // Generating the SQL WHERE part by concatenating the generated clauses.
0262     // The generated query will be in the form of '0 OR clause1 OR clause2 ...'
0263 
0264     const QString whereActivity = accumulate(activities | activityToWhereClause, QStringLiteral("0"));
0265 
0266     const QString whereAgent = accumulate(agents | agentToWhereClause, QStringLiteral("0"));
0267 
0268     // qDebug() << "This is the filter: " << '(' + whereActivity + ") AND (" + whereAgent + ')';
0269 
0270     return '(' + whereActivity + ") AND (" + whereAgent + ')';
0271 }
0272 
0273 void ResourceModel::reloadData()
0274 {
0275     m_sorting = m_config.readEntry(m_shownAgents.first(), QStringList());
0276 
0277     if (!m_database.isValid())
0278         return;
0279     m_databaseModel->setFilter(whereClause(m_shownActivities, m_shownAgents));
0280 }
0281 
0282 void ResourceModel::onCurrentActivityChanged(const QString &activity)
0283 {
0284     Q_UNUSED(activity);
0285 
0286     if (m_shownActivities.contains(":current")) {
0287         reloadData();
0288     }
0289 }
0290 
0291 QVariant ResourceModel::data(const QModelIndex &proxyIndex, int role) const
0292 {
0293     auto index = mapToSource(proxyIndex);
0294 
0295     if (role == Qt::DisplayRole || role == DescriptionRole || role == Qt::DecorationRole) {
0296         auto uri = dataForColumn(index, RESOURCE_COLUMN).toString();
0297 
0298         // TODO: Will probably need some more special handling -
0299         //       for application:/ and a few more
0300 
0301         if (uri.startsWith('/')) {
0302             uri = QLatin1String("file://") + uri;
0303         }
0304 
0305         KFileItem file(uri);
0306         // clang-format off
0307         if (file.mimetype() == "application/x-desktop") {
0308             KDesktopFile desktop(file.localPath());
0309 
0310             return role == Qt::DisplayRole    ? desktop.readGenericName() :
0311                    role == DescriptionRole    ? desktop.readName() :
0312                    role == Qt::DecorationRole ? desktop.readIcon() : QVariant();
0313         }
0314 
0315         return role == Qt::DisplayRole    ? file.name() :
0316                role == Qt::DecorationRole ? file.iconName() : QVariant();
0317     }
0318 
0319     return dataForColumn(index,
0320             role == ResourceRole ? RESOURCE_COLUMN :
0321             role == AgentRole    ? AGENT_COLUMN :
0322             role == ActivityRole ? ACTIVITY_COLUMN :
0323                                    UNKNOWN_COLUMN
0324         );
0325     // clang-format on
0326 }
0327 
0328 void ResourceModel::linkResourceToActivity(const QString &resource, const QJSValue &callback) const
0329 {
0330     linkResourceToActivity(resource, m_shownActivities.first(), callback);
0331 }
0332 
0333 void ResourceModel::linkResourceToActivity(const QString &resource, const QString &activity, const QJSValue &callback) const
0334 {
0335     linkResourceToActivity(m_shownAgents.first(), resource, activity, callback);
0336 }
0337 
0338 void ResourceModel::linkResourceToActivity(const QString &agent, const QString &_resource, const QString &activity, const QJSValue &callback) const
0339 {
0340     if (activity == ":any") {
0341         qWarning() << ":any is not a valid activity specification for linking";
0342         return;
0343     }
0344 
0345     auto resource = validateResource(_resource);
0346 
0347     // qDebug() << "ResourceModel: Linking resource to activity: --------------------------------------------------\n"
0348     //          << "ResourceModel:         Resource: " << resource << "\n"
0349     //          << "ResourceModel:         Agents: " << agent << "\n"
0350     //          << "ResourceModel:         Activities: " << activity << "\n";
0351 
0352     kamd::utils::continue_with(DBusFuture::asyncCall<void>(m_linker.get(),
0353                                                            QStringLiteral("LinkResourceToActivity"),
0354                                                            agent,
0355                                                            resource,
0356                                                            activity == ":current"      ? m_service.currentActivity()
0357                                                                : activity == ":global" ? ""
0358                                                                                        : activity),
0359                                callback);
0360 }
0361 
0362 void ResourceModel::unlinkResourceFromActivity(const QString &resource, const QJSValue &callback)
0363 {
0364     unlinkResourceFromActivity(m_shownAgents, resource, m_shownActivities, callback);
0365 }
0366 
0367 void ResourceModel::unlinkResourceFromActivity(const QString &resource, const QString &activity, const QJSValue &callback)
0368 {
0369     unlinkResourceFromActivity(m_shownAgents, resource, QStringList() << activity, callback);
0370 }
0371 
0372 void ResourceModel::unlinkResourceFromActivity(const QString &agent, const QString &resource, const QString &activity, const QJSValue &callback)
0373 {
0374     unlinkResourceFromActivity(QStringList() << agent, resource, QStringList() << activity, callback);
0375 }
0376 
0377 void ResourceModel::unlinkResourceFromActivity(const QStringList &agents, const QString &_resource, const QStringList &activities, const QJSValue &callback)
0378 {
0379     auto resource = validateResource(_resource);
0380 
0381     // qDebug() << "ResourceModel: Unlinking resource from activity: ----------------------------------------------\n"
0382     //          << "ResourceModel:         Resource: " << resource << "\n"
0383     //          << "ResourceModel:         Agents: " << agents << "\n"
0384     //          << "ResourceModel:         Activities: " << activities << "\n";
0385 
0386     for (const auto &agent : agents) {
0387         for (const auto &activity : activities) {
0388             if (activity == ":any") {
0389                 qWarning() << ":any is not a valid activity specification for linking";
0390                 return;
0391             }
0392 
0393             // We might want to compose the continuations into one
0394             // so that the callback gets called only once,
0395             // but we don't care about that at the moment
0396             kamd::utils::continue_with(DBusFuture::asyncCall<void>(m_linker.get(),
0397                                                                    QStringLiteral("UnlinkResourceFromActivity"),
0398                                                                    agent,
0399                                                                    resource,
0400                                                                    activity == ":current"      ? m_service.currentActivity()
0401                                                                        : activity == ":global" ? ""
0402                                                                                                : activity),
0403                                        callback);
0404         }
0405     }
0406 }
0407 
0408 bool ResourceModel::isResourceLinkedToActivity(const QString &resource)
0409 {
0410     return isResourceLinkedToActivity(m_shownAgents, resource, m_shownActivities);
0411 }
0412 
0413 bool ResourceModel::isResourceLinkedToActivity(const QString &resource, const QString &activity)
0414 {
0415     return isResourceLinkedToActivity(m_shownAgents, resource, QStringList() << activity);
0416 }
0417 
0418 bool ResourceModel::isResourceLinkedToActivity(const QString &agent, const QString &resource, const QString &activity)
0419 {
0420     return isResourceLinkedToActivity(QStringList() << agent, resource, QStringList() << activity);
0421 }
0422 
0423 bool ResourceModel::isResourceLinkedToActivity(const QStringList &agents, const QString &_resource, const QStringList &activities)
0424 {
0425     if (!m_database.isValid())
0426         return false;
0427 
0428     auto resource = validateResource(_resource);
0429 
0430     // qDebug() << "ResourceModel: Testing whether the resource is linked to activity: ----------------------------\n"
0431     //          << "ResourceModel:         Resource: " << resource << "\n"
0432     //          << "ResourceModel:         Agents: " << agents << "\n"
0433     //          << "ResourceModel:         Activities: " << activities << "\n";
0434 
0435     QSqlQuery query(m_database);
0436     query.prepare(
0437         "SELECT targettedResource "
0438         "FROM ResourceLink "
0439         "WHERE targettedResource=:resource AND "
0440         + whereClause(activities, agents));
0441     query.bindValue(":resource", resource);
0442     query.exec();
0443 
0444     auto result = query.next();
0445 
0446     // qDebug() << "Query: " << query.lastQuery();
0447     //
0448     // if (query.lastError().isValid()) {
0449     //     qDebug() << "Error: " << query.lastError();
0450     // }
0451     //
0452     // qDebug() << "Result: " << result;
0453 
0454     return result;
0455 }
0456 
0457 void ResourceModel::onResourceLinkedToActivity(const QString &initiatingAgent, const QString &targettedResource, const QString &usedActivity)
0458 {
0459     Q_UNUSED(targettedResource);
0460 
0461     if (!loadDatabase())
0462         return;
0463 
0464     auto matchingActivity = boost::find_if(m_shownActivities, [&](const QString &shownActivity) {
0465         return
0466             // If the activity is not important
0467             shownActivity == ":any" ||
0468             // or we are listening for the changes for the current activity
0469             (shownActivity == ":current" && usedActivity == m_service.currentActivity()) ||
0470             // or we want the globally linked resources
0471             (shownActivity == ":global" && usedActivity.isEmpty()) ||
0472             // or we have a specific activity in mind
0473             shownActivity == usedActivity;
0474     });
0475 
0476     auto matchingAgent = boost::find_if(m_shownAgents, [&](const QString &shownAgent) {
0477         return
0478             // If the agent is not important
0479             shownAgent == ":any" ||
0480             // or we are listening for the changes for the current agent
0481             (shownAgent == ":current" && initiatingAgent == QCoreApplication::applicationName()) ||
0482             // or for links that are global, and not related to a specific agent
0483             (shownAgent == ":global" && initiatingAgent.isEmpty()) ||
0484             // or we have a specific agent to listen for
0485             shownAgent == initiatingAgent;
0486     });
0487 
0488     if (matchingActivity != m_shownActivities.end() && matchingAgent != m_shownAgents.end()) {
0489         // TODO: This might be smarter possibly, but might collide
0490         //       with the SQL model. Implement a custom model with internal
0491         //       cache instead of basing it on QSqlModel.
0492         reloadData();
0493     }
0494 }
0495 
0496 void ResourceModel::onResourceUnlinkedFromActivity(const QString &initiatingAgent, const QString &targettedResource, const QString &usedActivity)
0497 {
0498     // These are the same at the moment
0499     onResourceLinkedToActivity(initiatingAgent, targettedResource, usedActivity);
0500 }
0501 
0502 void ResourceModel::setOrder(const QStringList &resources)
0503 {
0504     m_sorting = resources;
0505     m_config.writeEntry(m_shownAgents.first(), m_sorting);
0506     m_config.sync();
0507     invalidate();
0508 }
0509 
0510 void ResourceModel::move(int sourceItem, int destinationItem)
0511 {
0512     QStringList resources;
0513     const int rows = rowCount();
0514 
0515     for (int row = 0; row < rows; row++) {
0516         resources << resourceAt(row);
0517     }
0518 
0519     if (sourceItem < 0 || sourceItem >= rows || destinationItem < 0 || destinationItem >= rows) {
0520         return;
0521     }
0522 
0523     // Moving one item from the source item's location to the location
0524     // after the destination item
0525     std::rotate(resources.begin() + sourceItem, resources.begin() + sourceItem + 1, resources.begin() + destinationItem + 1);
0526 
0527     setOrder(resources);
0528 }
0529 
0530 void ResourceModel::sortItems(Qt::SortOrder sortOrder)
0531 {
0532     typedef QPair<QString, QString> Resource;
0533     QList<Resource> resources;
0534     const int rows = rowCount();
0535 
0536     for (int row = 0; row < rows; ++row) {
0537         resources << qMakePair(resourceAt(row), displayAt(row));
0538     }
0539 
0540     std::sort(resources.begin(), resources.end(), [sortOrder](const Resource &left, const Resource &right) {
0541         return sortOrder == Qt::AscendingOrder ? left.second < right.second : right.second < left.second;
0542     });
0543 
0544     QStringList result;
0545 
0546     for (const auto &resource : std::as_const(resources)) {
0547         result << resource.first;
0548     }
0549 
0550     setOrder(result);
0551 }
0552 
0553 KConfigGroup ResourceModel::config() const
0554 {
0555     return KSharedConfig::openConfig("kactivitymanagerd-resourcelinkingrc")->group("Order");
0556 }
0557 
0558 int ResourceModel::count() const
0559 {
0560     return QSortFilterProxyModel::rowCount();
0561 }
0562 
0563 QString ResourceModel::displayAt(int row) const
0564 {
0565     return data(index(row, 0), Qt::DisplayRole).toString();
0566 }
0567 
0568 QString ResourceModel::resourceAt(int row) const
0569 {
0570     return validateResource(data(index(row, 0), ResourceRole).toString());
0571 }
0572 
0573 void ResourceModel::loadDefaultsIfNeeded() const
0574 {
0575     // Did we get a request to actually do anything?
0576     if (m_defaultItemsConfig.isEmpty())
0577         return;
0578     if (m_shownAgents.size() == 0)
0579         return;
0580 
0581     // If we have already loaded the items, just exit
0582     if (m_defaultItemsLoaded)
0583         return;
0584     m_defaultItemsLoaded = true;
0585 
0586     // If there are items in the model, no need to load the defaults
0587     if (count() != 0)
0588         return;
0589 
0590     // Did we already load the defaults for this agent?
0591     QStringList alreadyInitialized = m_config.readEntry("defaultItemsProcessedFor", QStringList());
0592     if (alreadyInitialized.contains(m_shownAgents.first()))
0593         return;
0594     alreadyInitialized << m_shownAgents.first();
0595     m_config.writeEntry("defaultItemsProcessedFor", alreadyInitialized);
0596     m_config.sync();
0597 
0598     QStringList args = m_defaultItemsConfig.split("/");
0599     QString configField = args.takeLast();
0600     QString configGroup = args.takeLast();
0601     QString configFile = args.join("/");
0602 
0603     // qDebug() << "Config"
0604     //          << configFile << " "
0605     //          << configGroup << " "
0606     //          << configField << " ";
0607 
0608     QStringList items = KSharedConfig::openConfig(configFile)->group(configGroup).readEntry(configField, QStringList());
0609 
0610     for (const auto &item : items) {
0611         // qDebug() << "Adding: " << item;
0612         linkResourceToActivity(item, ":global", QJSValue());
0613     }
0614 }
0615 
0616 QString ResourceModel::validateResource(const QString &resource) const
0617 {
0618     return resource.startsWith(QLatin1String("file://")) ? QUrl(resource).toLocalFile() : resource;
0619 }
0620 
0621 } // namespace Imports
0622 } // namespace KActivities
0623 
0624 // #include "resourcemodel.moc"