File indexing completed on 2024-05-05 17:44:59

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