File indexing completed on 2025-01-26 05:06:07
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 "moc_resourcemodel.cpp"