File indexing completed on 2024-05-12 05:38:17

0001 /*
0002     SPDX-FileCopyrightText: 2016 Aleix Pol Gonzalez <aleixpol@kde.org>
0003 
0004     SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005 */
0006 
0007 #include "appstreamrunner.h"
0008 
0009 #include <unordered_set>
0010 
0011 #include <AppStreamQt/icon.h>
0012 
0013 #include <QDebug>
0014 #include <QDesktopServices>
0015 #include <QDir>
0016 #include <QIcon>
0017 #include <QTimer>
0018 
0019 #include <KApplicationTrader>
0020 #include <KIO/OpenUrlJob>
0021 #include <KLocalizedString>
0022 #include <KNotificationJobUiDelegate>
0023 #include <KSycoca>
0024 
0025 #include "debug.h"
0026 
0027 K_PLUGIN_CLASS_WITH_JSON(InstallerRunner, "plasma-runner-appstream.json")
0028 
0029 InstallerRunner::InstallerRunner(QObject *parent, const KPluginMetaData &metaData)
0030     : KRunner::AbstractRunner(parent, metaData)
0031 {
0032     addSyntax(":q:", i18n("Looks for non-installed components according to :q:"));
0033     setMinLetterCount(3);
0034 }
0035 
0036 static QIcon componentIcon(const AppStream::Component &comp)
0037 {
0038     QIcon ret;
0039     const auto icons = comp.icons();
0040     if (icons.isEmpty()) {
0041         ret = QIcon::fromTheme(QStringLiteral("package-x-generic"));
0042     } else
0043         for (const AppStream::Icon &icon : icons) {
0044             QStringList stock;
0045             switch (icon.kind()) {
0046             case AppStream::Icon::KindLocal:
0047                 ret.addFile(icon.url().toLocalFile(), icon.size());
0048                 break;
0049             case AppStream::Icon::KindCached:
0050                 ret.addFile(icon.url().toLocalFile(), icon.size());
0051                 break;
0052             case AppStream::Icon::KindStock:
0053                 stock += icon.name();
0054                 break;
0055             default:
0056                 break;
0057             }
0058             if (ret.isNull() && !stock.isEmpty()) {
0059                 ret = QIcon::fromTheme(stock.first());
0060             }
0061         }
0062     return ret;
0063 }
0064 
0065 void InstallerRunner::match(KRunner::RunnerContext &context)
0066 {
0067     // Give the other runners a bit of time to produce results
0068     QEventLoop loop;
0069     QTimer::singleShot(200, &loop, &QEventLoop::quit);
0070     loop.exec();
0071     if (!context.isValid()) {
0072         return;
0073     }
0074 
0075     // Check if other plugins have already found an executable, if that is the case we do
0076     // not want to ask the user to install anything else
0077     const QList<KRunner::QueryMatch> matches = context.matches();
0078     const bool execFound = std::any_of(matches.cbegin(), matches.cend(), [](const KRunner::QueryMatch &match) {
0079         return match.id().startsWith(QLatin1String("exec://"));
0080     });
0081     if (execFound) {
0082         return;
0083     }
0084 
0085     std::unordered_set<QString> uniqueIds;
0086     const auto components = findComponentsByString(context.query());
0087 
0088     for (auto it = components.cbegin(); it != components.cend() && uniqueIds.size() < 3; it = std::next(it)) {
0089         if (it->kind() != AppStream::Component::KindDesktopApp)
0090             continue;
0091 
0092         const QString componentId = it->id();
0093         const auto servicesFound = KApplicationTrader::query([&componentId](const KService::Ptr &service) {
0094             if (service->exec().isEmpty())
0095                 return false;
0096 
0097             if (service->desktopEntryName().compare(componentId, Qt::CaseInsensitive) == 0)
0098                 return true;
0099 
0100             const auto idWithoutDesktop = QString(componentId).remove(".desktop");
0101             if (service->desktopEntryName().compare(idWithoutDesktop, Qt::CaseInsensitive) == 0)
0102                 return true;
0103 
0104             const auto renamedFrom = service->property<QStringList>("X-Flatpak-RenamedFrom");
0105             if (renamedFrom.contains(componentId, Qt::CaseInsensitive) || renamedFrom.contains(idWithoutDesktop, Qt::CaseInsensitive))
0106                 return true;
0107 
0108             return false;
0109         });
0110 
0111         if (!servicesFound.isEmpty())
0112             continue;
0113         const auto [_, inserted] = uniqueIds.insert(componentId);
0114         if (!inserted) {
0115             continue;
0116         }
0117 
0118         KRunner::QueryMatch match(this);
0119         match.setCategoryRelevance(KRunner::QueryMatch::CategoryRelevance::Lowest); // Make sure it is less relavant than KCMs or apps
0120         match.setId(componentId);
0121         match.setIcon(componentIcon(*it));
0122         match.setText(i18n("Get %1…", it->name()));
0123         match.setSubtext(it->summary());
0124         match.setData(QUrl("appstream://" + componentId));
0125         match.setRelevance(it->name().compare(context.query(), Qt::CaseInsensitive) == 0 ? 1. : 0.7);
0126         context.addMatch(match);
0127     }
0128 }
0129 
0130 void InstallerRunner::run(const KRunner::RunnerContext & /*context*/, const KRunner::QueryMatch &match)
0131 {
0132     const QUrl appstreamUrl = match.data().toUrl();
0133 
0134     auto job = new KIO::OpenUrlJob(appstreamUrl);
0135     job->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoErrorHandlingEnabled));
0136     job->start();
0137 }
0138 
0139 void InstallerRunner::init()
0140 {
0141     // KApplicationTrader uses KService which uses KSycoca which holds
0142     // KDirWatch instances to monitor changes. We don't need this on
0143     // our runner threads - let's not needlessly allocate inotify instances.
0144     KSycoca::disableAutoRebuild();
0145 }
0146 
0147 QList<AppStream::Component> InstallerRunner::findComponentsByString(const QString &query)
0148 {
0149     static bool warnedOnce = false;
0150     static bool opened = m_db.load();
0151     if (!opened) {
0152         if (warnedOnce) {
0153             qCDebug(RUNNER_APPSTREAM) << "Had errors when loading AppStream metadata pool" << m_db.lastError();
0154         } else {
0155             qCWarning(RUNNER_APPSTREAM) << "Had errors when loading AppStream metadata pool" << m_db.lastError();
0156             warnedOnce = true;
0157         }
0158     }
0159 
0160     return m_db.search(query).toList();
0161 }
0162 
0163 #include "appstreamrunner.moc"