File indexing completed on 2024-11-17 04:55:38

0001 /*
0002  *   SPDX-FileCopyrightText: 2013 Aleix Pol Gonzalez <aleixpol@blue-systems.com>
0003  *   SPDX-FileCopyrightText: 2017 Jan Grulich <jgrulich@redhat.com>
0004  *
0005  *   SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0006  */
0007 
0008 #include "FlatpakBackend.h"
0009 #include "FlatpakFetchDataJob.h"
0010 #include "FlatpakJobTransaction.h"
0011 #include "FlatpakRefreshAppstreamMetadataJob.h"
0012 #include "FlatpakSourcesBackend.h"
0013 
0014 #include <ReviewsBackend/Rating.h>
0015 #include <Transaction/Transaction.h>
0016 #include <appstream/AppStreamUtils.h>
0017 #include <appstream/OdrsReviewsBackend.h>
0018 #include <resources/SourcesModel.h>
0019 #include <resources/StandardBackendUpdater.h>
0020 #include <utils.h>
0021 
0022 #include <AppStreamQt/bundle.h>
0023 #include <AppStreamQt/icon.h>
0024 #include <AppStreamQt/metadata.h>
0025 #include <AppStreamQt/pool.h>
0026 #include <AppStreamQt/release.h>
0027 #include <AppStreamQt/version.h>
0028 
0029 #include <KAboutData>
0030 #include <KConfigGroup>
0031 #include <KLocalizedString>
0032 #include <KPluginFactory>
0033 #include <KSharedConfig>
0034 
0035 #include <QDebug>
0036 #include <QDir>
0037 #include <QFile>
0038 #include <QFileInfo>
0039 #include <QFutureWatcher>
0040 #include <QNetworkAccessManager>
0041 #include <QSettings>
0042 #include <QTemporaryFile>
0043 #include <QTextStream>
0044 #include <QThread>
0045 #include <QTimer>
0046 #include <QtConcurrentRun>
0047 
0048 #include <QRegularExpression>
0049 #include <glib.h>
0050 
0051 #include <Category/Category.h>
0052 #include <optional>
0053 #include <set>
0054 #include <sys/stat.h>
0055 
0056 DISCOVER_BACKEND_PLUGIN(FlatpakBackend)
0057 
0058 using namespace Qt::Literals::StringLiterals;
0059 
0060 class FlatpakSource
0061 {
0062 public:
0063     FlatpakSource(FlatpakBackend *backend, FlatpakInstallation *installation)
0064         : m_remote(nullptr)
0065         , m_installation(installation)
0066         , m_backend(backend)
0067     {
0068         g_object_ref(m_installation);
0069     }
0070 
0071     FlatpakSource(FlatpakBackend *backend, FlatpakInstallation *installation, FlatpakRemote *remote)
0072         : m_remote(remote)
0073         , m_installation(installation)
0074         , m_backend(backend)
0075         , m_appstreamIconsDir(appstreamDir() + QLatin1String("/icons"))
0076     {
0077         g_object_ref(m_remote);
0078         g_object_ref(m_installation);
0079     }
0080 
0081     ~FlatpakSource()
0082     {
0083         if (m_remote) {
0084             g_object_unref(m_remote);
0085         }
0086         g_object_unref(m_installation);
0087     }
0088 
0089     QString url() const
0090     {
0091         return m_remote ? QString::fromUtf8(flatpak_remote_get_url(m_remote)) : QString();
0092     }
0093 
0094     bool isEnabled() const
0095     {
0096         return m_remote && !flatpak_remote_get_disabled(m_remote);
0097     }
0098 
0099     QString appstreamIconsDir() const
0100     {
0101         return m_appstreamIconsDir;
0102     }
0103     QString appstreamDir() const
0104     {
0105         Q_ASSERT(m_remote);
0106         g_autoptr(GFile) appstreamDir = flatpak_remote_get_appstream_dir(m_remote, nullptr);
0107         if (!appstreamDir) {
0108             qWarning() << "No appstream dir for" << flatpak_remote_get_name(m_remote);
0109             return {};
0110         }
0111         g_autofree char *path_str = g_file_get_path(appstreamDir);
0112         return QString::fromUtf8(path_str);
0113     }
0114 
0115     QString name() const
0116     {
0117         return m_remote ? QString::fromUtf8(flatpak_remote_get_name(m_remote)) : QString();
0118     }
0119 
0120     QString title() const
0121     {
0122         auto ret = m_remote ? QString::fromUtf8(flatpak_remote_get_title(m_remote)) : QString();
0123         if (flatpak_installation_get_is_user(m_installation)) {
0124             ret = i18nc("user denotes this as user-scoped flatpak repo", "%1 (user)", ret);
0125         }
0126         return ret;
0127     }
0128 
0129     FlatpakInstallation *installation() const
0130     {
0131         return m_installation;
0132     }
0133 
0134     void addResource(FlatpakResource *resource)
0135     {
0136         // Update app with all possible information we have
0137         if (!m_backend->parseMetadataFromAppBundle(resource)) {
0138             qWarning() << "Failed to parse metadata from app bundle for" << resource->name();
0139         }
0140 
0141         m_backend->updateAppState(resource);
0142 
0143         Q_ASSERT(!m_resources.contains(resource->uniqueId()) || m_resources.value(resource->uniqueId()) == resource);
0144         m_resources.insert(resource->uniqueId(), resource);
0145 
0146         QObject::connect(resource, &FlatpakResource::sizeChanged, m_backend, [this, resource] {
0147             if (!m_backend->isFetching())
0148                 Q_EMIT m_backend->resourcesChanged(resource, {"size", "sizeDescription"});
0149         });
0150     }
0151 
0152     FlatpakRemote *remote() const
0153     {
0154         return m_remote;
0155     }
0156 
0157     AppStream::ComponentBox componentsByName(const QString &name)
0158     {
0159         auto comps = m_pool->componentsById(name);
0160         if (!comps.isEmpty()) {
0161             return comps;
0162         }
0163 
0164         comps = m_pool->componentsByProvided(AppStream::Provided::KindId, name);
0165         return comps;
0166     }
0167 
0168     AppStream::ComponentBox componentsByFlatpakId(const QString &ref)
0169     {
0170         AppStream::ComponentBox comps = m_pool->componentsByBundleId(AppStream::Bundle::KindFlatpak, ref, false);
0171         if (!comps.isEmpty())
0172             return comps;
0173 
0174         comps = m_pool->componentsByProvided(AppStream::Provided::KindId, ref.section('/'_L1, 1, 1));
0175         return comps;
0176     }
0177 
0178     AppStream::Pool *m_pool = nullptr;
0179     QHash<FlatpakResource::Id, FlatpakResource *> m_resources;
0180 
0181 private:
0182     FlatpakRemote *const m_remote;
0183     FlatpakInstallation *const m_installation;
0184     FlatpakBackend *const m_backend;
0185     const QString m_appstreamIconsDir;
0186 };
0187 
0188 static void populateRemote(FlatpakRemote *remote, const QString &name, const QString &url, const QString &gpgKey)
0189 {
0190     flatpak_remote_set_url(remote, url.toUtf8().constData());
0191     flatpak_remote_set_noenumerate(remote, false);
0192     flatpak_remote_set_title(remote, name.toUtf8().constData());
0193 
0194     if (!gpgKey.isEmpty()) {
0195         gsize dataLen = 0;
0196         g_autofree guchar *data = nullptr;
0197         g_autoptr(GBytes) bytes = nullptr;
0198         data = g_base64_decode(gpgKey.toUtf8().constData(), &dataLen);
0199         bytes = g_bytes_new(data, dataLen);
0200         flatpak_remote_set_gpg_verify(remote, true);
0201         flatpak_remote_set_gpg_key(remote, bytes);
0202     } else {
0203         flatpak_remote_set_gpg_verify(remote, false);
0204     }
0205 }
0206 
0207 QDebug operator<<(QDebug debug, const FlatpakResource::Id &id)
0208 {
0209     QDebugStateSaver saver(debug);
0210     debug.nospace() << "FlatpakResource::Id(";
0211     debug.nospace() << "name:" << id.id << ',';
0212     debug.nospace() << "branch:" << id.branch;
0213     debug.nospace() << ')';
0214     return debug;
0215 }
0216 
0217 static FlatpakResource::Id idForComponent(const AppStream::Component &component)
0218 {
0219     // app/app.getspace.Space/x86_64/stable
0220     const QString bundleId = component.bundle(AppStream::Bundle::KindFlatpak).id();
0221     auto parts = QStringView(bundleId).split('/'_L1);
0222     Q_ASSERT(parts.size() == 4);
0223 
0224     return {
0225         component.id(),
0226         parts[3].toString(),
0227         parts[2].toString(),
0228     };
0229 }
0230 
0231 static FlatpakResource::Id idForInstalledRef(FlatpakInstalledRef *ref, const QString &postfix = QString())
0232 {
0233     const QString appId = QLatin1String(flatpak_ref_get_name(FLATPAK_REF(ref))) + postfix;
0234     const QString arch = QString::fromUtf8(flatpak_ref_get_arch(FLATPAK_REF(ref)));
0235     const QString branch = QString::fromUtf8(flatpak_ref_get_branch(FLATPAK_REF(ref)));
0236 
0237     return {appId, branch, arch};
0238 }
0239 
0240 static std::optional<AppStream::Metadata> metadataFromBytes(GBytes *appstreamGz, GCancellable *cancellable)
0241 {
0242     g_autoptr(GError) localError = nullptr;
0243     g_autoptr(GZlibDecompressor) decompressor = nullptr;
0244     g_autoptr(GInputStream) streamGz = nullptr;
0245     g_autoptr(GInputStream) streamData = nullptr;
0246     g_autoptr(GBytes) appstream = nullptr;
0247 
0248     /* decompress data */
0249     decompressor = g_zlib_decompressor_new(G_ZLIB_COMPRESSOR_FORMAT_GZIP);
0250     streamGz = g_memory_input_stream_new_from_bytes(appstreamGz);
0251     if (!streamGz) {
0252         return {};
0253     }
0254 
0255     streamData = g_converter_input_stream_new(streamGz, G_CONVERTER(decompressor));
0256 
0257     appstream = g_input_stream_read_bytes(streamData, 0x100000, cancellable, &localError);
0258     if (!appstream) {
0259         qWarning() << "Failed to extract appstream metadata from bundle:" << localError->message;
0260         return {};
0261     }
0262 
0263     gsize len = 0;
0264     gconstpointer data = g_bytes_get_data(appstream, &len);
0265 
0266     AppStream::Metadata metadata;
0267 #if ASQ_CHECK_VERSION(0, 16, 0)
0268     metadata.setFormatStyle(AppStream::Metadata::FormatStyleCatalog);
0269 #else
0270     metadata.setFormatStyle(AppStream::Metadata::FormatStyleCollection);
0271 #endif
0272     AppStream::Metadata::MetadataError error = metadata.parse(QString::fromUtf8((char *)data, len), AppStream::Metadata::FormatKindXml);
0273     if (error != AppStream::Metadata::MetadataErrorNoError) {
0274         qWarning() << "Failed to parse appstream metadata: " << error;
0275         return {};
0276     }
0277     return metadata;
0278 }
0279 
0280 FlatpakBackend::FlatpakBackend(QObject *parent)
0281     : AbstractResourcesBackend(parent)
0282     , m_updater(new StandardBackendUpdater(this))
0283     , m_reviews(OdrsReviewsBackend::global())
0284     , m_cancellable(g_cancellable_new())
0285     , m_checkForUpdatesTimer(new QTimer(this))
0286 {
0287     g_autoptr(GError) error = nullptr;
0288 
0289     connect(m_updater, &StandardBackendUpdater::updatesCountChanged, this, &FlatpakBackend::updatesCountChanged);
0290 
0291     // Load flatpak installation
0292     if (!setupFlatpakInstallations(&error)) {
0293         qWarning() << "Failed to setup flatpak installations:" << error->message;
0294     } else {
0295         m_sources = new FlatpakSourcesBackend(m_installations, this);
0296         loadAppsFromAppstreamData();
0297 
0298         SourcesModel::global()->addSourcesBackend(m_sources);
0299     }
0300 
0301     connect(m_reviews.data(), &OdrsReviewsBackend::ratingsReady, this, [this] {
0302         m_reviews->emitRatingFetched(this, kAppend<QList<AbstractResource *>>(m_flatpakSources, [](const auto &source) {
0303                                          return kTransform<QList<AbstractResource *>>(source->m_resources.values());
0304                                      }));
0305     });
0306 
0307     m_checkForUpdatesTimer->setInterval(1000);
0308     m_checkForUpdatesTimer->setSingleShot(true);
0309     connect(m_checkForUpdatesTimer, &QTimer::timeout, this, &FlatpakBackend::checkForUpdates);
0310 
0311     /* Override the umask to 022 to make it possible to share files between
0312      * the plasma-discover process and flatpak system helper process.
0313      *
0314      * See https://github.com/flatpak/flatpak/pull/2856/
0315      */
0316     umask(022);
0317 }
0318 
0319 FlatpakBackend::~FlatpakBackend()
0320 {
0321     g_cancellable_cancel(m_cancellable);
0322     if (!m_threadPool.waitForDone(200)) {
0323         qDebug() << "could not kill them all" << m_threadPool.activeThreadCount();
0324     }
0325     m_threadPool.clear();
0326 
0327     for (auto inst : std::as_const(m_installations))
0328         g_object_unref(inst);
0329     m_installations.clear();
0330     g_object_unref(m_cancellable);
0331 }
0332 
0333 bool FlatpakBackend::isValid() const
0334 {
0335     return m_sources && !m_installations.isEmpty();
0336 }
0337 
0338 class FlatpakFetchRemoteResourceJob : public QNetworkAccessManager
0339 {
0340     Q_OBJECT
0341 public:
0342     FlatpakFetchRemoteResourceJob(const QUrl &url, ResultsStream *stream, FlatpakBackend *backend)
0343         : QNetworkAccessManager(backend)
0344         , m_backend(backend)
0345         , m_stream(stream)
0346         , m_url(url)
0347     {
0348         connect(stream, &ResultsStream::destroyed, this, &QObject::deleteLater);
0349     }
0350 
0351     void start()
0352     {
0353         if (m_url.isLocalFile()) {
0354             QTimer::singleShot(0, m_stream, [this] {
0355                 processFile(m_url);
0356             });
0357             return;
0358         }
0359 
0360         QNetworkRequest req(m_url);
0361         req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
0362         auto replyGet = get(req);
0363         connect(replyGet, &QNetworkReply::finished, this, [this, replyGet] {
0364             QScopedPointer<QNetworkReply, QScopedPointerDeleteLater> replyPtr(replyGet);
0365             if (replyGet->error() != QNetworkReply::NoError) {
0366                 qWarning() << "couldn't download" << m_url << replyGet->errorString();
0367                 m_stream->finish();
0368                 return;
0369             }
0370             const QUrl fileUrl = QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::TempLocation) //
0371                                                      + QLatin1Char('/') + m_url.fileName());
0372             auto replyPut = put(QNetworkRequest(fileUrl), replyGet->readAll());
0373             connect(replyPut, &QNetworkReply::finished, this, [this, fileUrl, replyPut]() {
0374                 QScopedPointer<QNetworkReply, QScopedPointerDeleteLater> replyPtr(replyPut);
0375                 if (replyPut->error() != QNetworkReply::NoError) {
0376                     qWarning() << "couldn't save" << m_url << replyPut->errorString();
0377                     m_stream->finish();
0378                     return;
0379                 }
0380                 if (!fileUrl.isLocalFile()) {
0381                     m_stream->finish();
0382                     return;
0383                 }
0384 
0385                 processFile(fileUrl);
0386             });
0387         });
0388     }
0389 
0390     void processFile(const QUrl &fileUrl)
0391     {
0392         const auto path = fileUrl.toLocalFile();
0393         if (path.endsWith(QLatin1String(".flatpak"))) {
0394             m_backend->addAppFromFlatpakBundle(fileUrl, m_stream);
0395         } else if (path.endsWith(QLatin1String(".flatpakref"))) {
0396             m_backend->addAppFromFlatpakRef(fileUrl, m_stream);
0397         } else if (path.endsWith(QLatin1String(".flatpakrepo"))) {
0398             m_backend->addSourceFromFlatpakRepo(fileUrl, m_stream);
0399         } else {
0400             qWarning() << "unrecognized format" << fileUrl;
0401         }
0402     }
0403 
0404 private:
0405     FlatpakBackend *const m_backend;
0406     ResultsStream *const m_stream;
0407     const QUrl m_url;
0408 };
0409 
0410 FlatpakRemote *FlatpakBackend::getFlatpakRemoteByUrl(const QString &url, FlatpakInstallation *installation) const
0411 {
0412     auto remotes = flatpak_installation_list_remotes(installation, m_cancellable, nullptr);
0413     if (!remotes) {
0414         return nullptr;
0415     }
0416 
0417     const QByteArray comparableUrl = url.toUtf8();
0418     for (uint i = 0; i < remotes->len; i++) {
0419         FlatpakRemote *remote = FLATPAK_REMOTE(g_ptr_array_index(remotes, i));
0420 
0421         if (comparableUrl == flatpak_remote_get_url(remote)) {
0422             return remote;
0423         }
0424     }
0425     return nullptr;
0426 }
0427 
0428 FlatpakInstalledRef *FlatpakBackend::getInstalledRefForApp(const FlatpakResource *resource) const
0429 {
0430     Q_ASSERT(resource->resourceType() != FlatpakResource::Source);
0431     g_autoptr(GError) localError = nullptr;
0432 
0433     const auto type = resource->resourceType() == FlatpakResource::DesktopApp ? FLATPAK_REF_KIND_APP : FLATPAK_REF_KIND_RUNTIME;
0434 
0435     FlatpakInstalledRef *ref = flatpak_installation_get_installed_ref(resource->installation(),
0436                                                                       type,
0437                                                                       resource->flatpakName().toUtf8().constData(),
0438                                                                       resource->arch().toUtf8().constData(),
0439                                                                       resource->branch().toUtf8().constData(),
0440                                                                       m_cancellable,
0441                                                                       &localError);
0442     return ref;
0443 }
0444 
0445 static QString refToBundleId(FlatpakRef *ref)
0446 {
0447     const QString typeAsString = flatpak_ref_get_kind(ref) == FLATPAK_REF_KIND_APP ? QStringLiteral("app") : QStringLiteral("runtime");
0448     const QString flatpakName = QString::fromUtf8(flatpak_ref_get_name(ref));
0449     const QString arch = QString::fromUtf8(flatpak_ref_get_arch(ref));
0450     const QString branch = QString::fromUtf8(flatpak_ref_get_branch(ref));
0451     return QStringLiteral("%1/%2/%3/%4").arg(typeAsString, flatpakName, arch, branch);
0452 }
0453 
0454 FlatpakResource *FlatpakBackend::getAppForInstalledRef(FlatpakInstallation *installation, FlatpakInstalledRef *ref, bool *freshResource) const
0455 {
0456     if (freshResource)
0457         *freshResource = false;
0458     const QString origin = QString::fromUtf8(flatpak_installed_ref_get_origin(ref));
0459     auto source = findSource(installation, origin);
0460     if (source) {
0461         auto ret = source->m_resources.value(idForInstalledRef(ref, {}));
0462         if (ret) {
0463             return ret;
0464         }
0465     }
0466 
0467     const QLatin1String name(flatpak_ref_get_name(FLATPAK_REF(ref)));
0468     const QLatin1String branch(flatpak_ref_get_branch(FLATPAK_REF(ref)));
0469     const QString pathExports = FlatpakResource::installationPath(installation) + QLatin1String("/exports/");
0470     const QString pathApps = pathExports + QLatin1String("share/applications/");
0471     const QString refId = refToBundleId(FLATPAK_REF(ref));
0472     AppStream::Component cid;
0473     if (source && source->m_pool) {
0474         auto comps = source->componentsByFlatpakId(refId);
0475         if (comps.isEmpty()) {
0476             g_autoptr(GBytes) metadata = flatpak_installed_ref_load_appdata(ref, m_cancellable, nullptr);
0477             if (metadata) {
0478                 auto meta = metadataFromBytes(metadata, m_cancellable);
0479                 comps = meta->components();
0480             }
0481         }
0482 
0483         if (comps.size() >= 1) {
0484             Q_ASSERT(comps.size() == 1);
0485             cid = *comps.indexSafe(0);
0486         }
0487     }
0488 
0489     if (!cid.isValid()) {
0490         AppStream::Metadata metadata;
0491         const QString fnDesktop = pathApps + name + QLatin1String(".desktop");
0492         AppStream::Metadata::MetadataError error = metadata.parseFile(fnDesktop, AppStream::Metadata::FormatKindDesktopEntry);
0493         if (error != AppStream::Metadata::MetadataErrorNoError) {
0494             if (QFile::exists(fnDesktop))
0495                 qDebug() << "Failed to parse appstream metadata:" << error << fnDesktop;
0496 
0497             cid.setId(name);
0498 #if FLATPAK_CHECK_VERSION(1, 1, 2)
0499             cid.setName(QString::fromUtf8(flatpak_installed_ref_get_appdata_name(ref)));
0500 #endif
0501         } else
0502             cid = metadata.component();
0503     }
0504 
0505     if (cid.bundle(AppStream::Bundle::KindFlatpak).isEmpty()) {
0506         AppStream::Bundle b;
0507         b.setKind(AppStream::Bundle::KindFlatpak);
0508         b.setId(refId);
0509         cid.addBundle(b);
0510     }
0511 
0512     if (source && cid.isValid()) {
0513         auto ret = source->m_resources.value(idForComponent(cid));
0514         if (ret) {
0515             return ret;
0516         }
0517     }
0518 
0519     if (!source) {
0520         return nullptr;
0521     }
0522 
0523     FlatpakResource *resource = new FlatpakResource(cid, source->installation(), const_cast<FlatpakBackend *>(this));
0524     resource->setOrigin(source->name());
0525     resource->setDisplayOrigin(source->title());
0526     resource->setIconPath(pathExports);
0527     resource->updateFromRef(FLATPAK_REF(ref));
0528     resource->setState(AbstractResource::Installed);
0529     source->addResource(resource);
0530 
0531     if (freshResource)
0532         *freshResource = true;
0533 
0534     Q_ASSERT(resource->uniqueId() == idForInstalledRef(ref) || resource->uniqueId() == idForInstalledRef(ref, QStringLiteral(".desktop")));
0535     return resource;
0536 }
0537 
0538 QSharedPointer<FlatpakSource> FlatpakBackend::findSource(FlatpakInstallation *installation, const QString &origin) const
0539 {
0540     for (const auto &source : m_flatpakSources) {
0541         if (source->installation() == installation && source->name() == origin) {
0542             return source;
0543         }
0544     }
0545     for (const auto &source : m_flatpakLoadingSources) {
0546         if (source->installation() == installation && source->name() == origin) {
0547             return source;
0548         }
0549     }
0550 
0551     qWarning() << "Could not find source:" << installation << origin;
0552     return {};
0553 }
0554 
0555 FlatpakResource *FlatpakBackend::getRuntimeForApp(FlatpakResource *resource) const
0556 {
0557     FlatpakResource *runtime = nullptr;
0558     const QString runtimeName = resource->runtime();
0559     const auto runtimeInfo = QStringView(runtimeName).split(QLatin1Char('/'));
0560 
0561     if (runtimeInfo.count() != 3) {
0562         return runtime;
0563     }
0564 
0565     for (const auto &source : m_flatpakSources) {
0566         for (auto it = source->m_resources.constBegin(), itEnd = source->m_resources.constEnd(); it != itEnd; ++it) {
0567             const auto &id = it.key();
0568             if ((*it)->resourceType() == FlatpakResource::Runtime && id.id == runtimeInfo.at(0) && id.branch == runtimeInfo.at(2)) {
0569                 runtime = *it;
0570                 break;
0571             }
0572         }
0573     }
0574 
0575     for (auto installation : m_installations) {
0576         auto instref = flatpak_installation_get_installed_ref(installation,
0577                                                               FLATPAK_REF_KIND_RUNTIME,
0578                                                               runtimeInfo.at(0).toUtf8().constData(),
0579                                                               runtimeInfo.at(1).toUtf8().constData(),
0580                                                               runtimeInfo.at(2).toUtf8().constData(),
0581                                                               m_cancellable,
0582                                                               nullptr);
0583         if (instref) {
0584             return getAppForInstalledRef(installation, instref);
0585         }
0586     }
0587 
0588     // TODO if runtime wasn't found, create a new one from available info
0589     if (!runtime) {
0590         qWarning() << "could not find runtime" << runtimeName << resource;
0591     }
0592 
0593     return runtime;
0594 }
0595 
0596 void FlatpakBackend::addAppFromFlatpakBundle(const QUrl &url, ResultsStream *stream)
0597 {
0598     auto x = qScopeGuard([stream] {
0599         stream->finish();
0600     });
0601     g_autoptr(GBytes) appstreamGz = nullptr;
0602     g_autoptr(GError) localError = nullptr;
0603     g_autoptr(GFile) file = nullptr;
0604     g_autoptr(FlatpakBundleRef) bundleRef = nullptr;
0605     AppStream::Component asComponent;
0606 
0607     file = g_file_new_for_path(url.toLocalFile().toUtf8().constData());
0608     bundleRef = flatpak_bundle_ref_new(file, &localError);
0609 
0610     if (!bundleRef) {
0611         qWarning() << "Failed to load bundle:" << localError->message;
0612         return;
0613     }
0614 
0615     gsize len = 0;
0616     g_autoptr(GBytes) metadata = flatpak_bundle_ref_get_metadata(bundleRef);
0617     const QByteArray metadataContent((char *)g_bytes_get_data(metadata, &len));
0618 
0619     appstreamGz = flatpak_bundle_ref_get_appstream(bundleRef);
0620     if (appstreamGz) {
0621         const auto metadata = metadataFromBytes(appstreamGz, m_cancellable);
0622         if (!metadata.has_value()) {
0623             return;
0624         }
0625 
0626         if (std::optional<AppStream::Component> firstComponent = metadata->components().indexSafe(0); firstComponent.has_value()) {
0627             asComponent = *firstComponent;
0628         } else {
0629             qWarning() << "Failed to parse appstream metadata";
0630             return;
0631         }
0632     } else {
0633         qWarning() << "No appstream metadata in bundle";
0634 
0635         QTemporaryFile tempFile;
0636         tempFile.setAutoRemove(false);
0637         if (!tempFile.open()) {
0638             qWarning() << "Failed to get metadata file";
0639             return;
0640         }
0641 
0642         tempFile.write(metadataContent);
0643         tempFile.close();
0644 
0645         // Parse the temporary file
0646         QSettings setting(tempFile.fileName(), QSettings::NativeFormat);
0647         setting.beginGroup(QLatin1String("Application"));
0648 
0649         asComponent.setName(setting.value(QLatin1String("name")).toString());
0650 
0651         tempFile.remove();
0652     }
0653 
0654     g_autoptr(GPtrArray) refs = flatpak_installation_list_installed_refs(preferredInstallation(), m_cancellable, &localError);
0655     if (!refs) {
0656         qWarning() << "Failed to get list of installed refs for listing local updates:" << localError->message;
0657         return;
0658     }
0659 
0660     for (uint i = 0; i < refs->len; i++) {
0661         FlatpakRef *ref = FLATPAK_REF(g_ptr_array_index(refs, i));
0662         FlatpakInstalledRef *iref = FLATPAK_INSTALLED_REF(g_ptr_array_index(refs, i));
0663         if (qstrcmp(flatpak_ref_get_commit(ref), flatpak_ref_get_commit(FLATPAK_REF(bundleRef))) == 0) {
0664             auto res = getAppForInstalledRef(preferredInstallation(), iref, nullptr);
0665             if (res) {
0666                 Q_EMIT stream->resourcesFound({res});
0667             }
0668             return;
0669         }
0670     }
0671 
0672     FlatpakResource *resource = new FlatpakResource(asComponent, preferredInstallation(), this);
0673     if (!updateAppMetadata(resource, metadataContent)) {
0674         delete resource;
0675         qWarning() << "Failed to update metadata from app bundle";
0676         return;
0677     }
0678 
0679     g_autoptr(GBytes) iconData = flatpak_bundle_ref_get_icon(bundleRef, 128);
0680     if (!iconData) {
0681         iconData = flatpak_bundle_ref_get_icon(bundleRef, 64);
0682     }
0683 
0684     if (iconData) {
0685         gsize len = 0;
0686         char *data = (char *)g_bytes_get_data(iconData, &len);
0687 
0688         QPixmap pixmap;
0689         pixmap.loadFromData(QByteArray(data, len), "PNG");
0690         resource->setBundledIcon(pixmap);
0691     }
0692 
0693     const QString origin = QString::fromUtf8(flatpak_bundle_ref_get_origin(bundleRef));
0694     resource->updateFromRef(FLATPAK_REF(bundleRef));
0695     resource->setDownloadSize(0);
0696     resource->setInstalledSize(flatpak_bundle_ref_get_installed_size(bundleRef));
0697     resource->setPropertyState(FlatpakResource::DownloadSize, FlatpakResource::AlreadyKnown);
0698     resource->setPropertyState(FlatpakResource::InstalledSize, FlatpakResource::AlreadyKnown);
0699     resource->setFlatpakFileType(FlatpakResource::FileFlatpak);
0700     resource->setOrigin(origin.isEmpty() ? i18n("Local bundle") : origin);
0701     resource->setResourceFile(url);
0702     resource->setState(FlatpakResource::None);
0703 
0704     if (!m_localSource) {
0705         m_localSource.reset(new FlatpakSource(this, preferredInstallation()));
0706         m_flatpakSources += m_localSource;
0707     }
0708     m_localSource->addResource(resource);
0709     Q_EMIT stream->resourcesFound({resource});
0710 }
0711 
0712 QString composeRef(bool isRuntime, const QString &name, const QString &branch)
0713 {
0714     return (isRuntime ? "runtime/"_L1 : "app/"_L1) + name + '/'_L1 + QString::fromUtf8(flatpak_get_default_arch()) + '/'_L1 + branch;
0715 }
0716 
0717 AppStream::Component fetchComponentFromRemote(const QSettings &settings, GCancellable *cancellable)
0718 {
0719     const QString name = settings.value(QStringLiteral("Flatpak Ref/Name")).toString();
0720     const QString branch = settings.value(QStringLiteral("Flatpak Ref/Branch")).toString();
0721     const QString remoteName = settings.value(QStringLiteral("Flatpak Ref/SuggestRemoteName")).toString();
0722     const bool isRuntime = settings.value(QStringLiteral("Flatpak Ref/IsRuntime")).toBool();
0723 
0724     AppStream::Component asComponent;
0725     asComponent.addUrl(AppStream::Component::UrlKindHomepage, settings.value(QStringLiteral("Flatpak Ref/Homepage")).toString());
0726     asComponent.setDescription(settings.value(QStringLiteral("Flatpak Ref/Description")).toString());
0727     asComponent.setName(settings.value(QStringLiteral("Flatpak Ref/Title")).toString());
0728     asComponent.setSummary(settings.value(QStringLiteral("Flatpak Ref/Comment")).toString());
0729     asComponent.setId(name);
0730 
0731     AppStream::Bundle b;
0732     b.setKind(AppStream::Bundle::KindFlatpak);
0733     b.setId(composeRef(isRuntime, asComponent.name(), branch));
0734     asComponent.addBundle(b);
0735 
0736     // We are going to create a temporary installation and add the remote to it.
0737     // There we will fetch the appstream metadata and then delete that temporary installation.
0738 
0739     g_autoptr(GError) localError = nullptr;
0740     const QString path = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + QLatin1String("/discover-flatpak-temporary-") + remoteName;
0741     qDebug() << "Creating temporary installation" << path;
0742     g_autoptr(GFile) file = g_file_new_for_path(QFile::encodeName(path).constData());
0743     g_autoptr(FlatpakInstallation) tempInstallation = flatpak_installation_new_for_path(file, true, cancellable, &localError);
0744     if (!tempInstallation) {
0745         return asComponent;
0746     }
0747     auto x = qScopeGuard([path] {
0748         QDir(path).removeRecursively();
0749     });
0750 
0751     g_autoptr(FlatpakRemote) tempRemote = flatpak_remote_new(remoteName.toUtf8().constData());
0752     populateRemote(tempRemote,
0753                    remoteName,
0754                    settings.value(QStringLiteral("Flatpak Ref/Url")).toString(),
0755                    settings.value(QStringLiteral("Flatpak Ref/GPGKey")).toString());
0756     if (!flatpak_installation_modify_remote(tempInstallation, tempRemote, cancellable, &localError)) {
0757         qDebug() << "error adding temporary remote" << localError->message;
0758         return asComponent;
0759     }
0760 
0761     auto cb = [](const char *status, guint progress, gboolean /*estimating*/, gpointer /*user_data*/) {
0762         qDebug() << "Progress..." << status << progress;
0763     };
0764 
0765     gboolean changed;
0766     if (!flatpak_installation_update_appstream_full_sync(tempInstallation,
0767                                                          remoteName.toUtf8().constData(),
0768                                                          nullptr,
0769                                                          cb,
0770                                                          nullptr,
0771                                                          &changed,
0772                                                          cancellable,
0773                                                          &localError)) {
0774         qDebug() << "error fetching appstream" << localError->message;
0775         return asComponent;
0776     }
0777     Q_ASSERT(changed);
0778     const QString appstreamLocation = path + "/appstream/"_L1 + remoteName + '/'_L1 + QString::fromUtf8(flatpak_get_default_arch()) + "/active"_L1;
0779 
0780     AppStream::Pool pool;
0781 #ifdef APPSTREAM_NEW_POOL_API
0782     pool.setLoadStdDataLocations(false);
0783 #if ASQ_CHECK_VERSION(0, 16, 0)
0784     pool.addExtraDataLocation(appstreamLocation, AppStream::Metadata::FormatStyleCatalog);
0785 #else
0786     pool.addExtraDataLocation(appstreamLocation, AppStream::Metadata::FormatStyleCollection);
0787 #endif
0788 #else
0789     pool.clearMetadataLocations();
0790     pool.addMetadataLocation(appstreamLocation);
0791     pool.setFlags(AppStream::Pool::FlagReadCollection);
0792     pool.setCacheFlags(AppStream::Pool::CacheFlagUseUser);
0793 
0794     const QString subdir = flatpak_installation_get_id(tempInstallation) + QLatin1Char('/') + remoteName;
0795     pool.setCacheLocation(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/flatpak-appstream-temp/" + subdir);
0796     QDir().mkpath(pool.cacheLocation());
0797 #endif
0798 
0799     if (!pool.load()) {
0800         qDebug() << "error loading pool" << pool.lastError();
0801         return asComponent;
0802     }
0803 
0804     // TODO optimise, this lookup should happen in libappstream
0805     auto comps = pool.components();
0806     kFilterInPlace<AppStream::ComponentBox>(comps, [name, branch](const AppStream::Component &component) {
0807         const QString id = component.bundle(AppStream::Bundle::KindFlatpak).id();
0808         // app/app.getspace.Space/x86_64/stable
0809         return id.section(QLatin1Char('/'), 1, 1) == name && (branch.isEmpty() || id.section(QLatin1Char('/'), 3, 3) == branch);
0810     });
0811     if (comps.isEmpty()) {
0812         qDebug() << "could not find" << name << "in" << remoteName;
0813         return asComponent;
0814     }
0815     return *comps.indexSafe(0);
0816 }
0817 
0818 void FlatpakBackend::addAppFromFlatpakRef(const QUrl &url, ResultsStream *stream)
0819 {
0820     Q_ASSERT(url.isLocalFile());
0821     QSettings settings(url.toLocalFile(), QSettings::NativeFormat);
0822     const QString refurl = settings.value(QStringLiteral("Flatpak Ref/Url")).toString();
0823     const QString name = settings.value(QStringLiteral("Flatpak Ref/Name")).toString();
0824     const QString remoteName = settings.value(QStringLiteral("Flatpak Ref/SuggestRemoteName")).toString();
0825     const QString branch = settings.value(QStringLiteral("Flatpak Ref/Branch")).toString();
0826     const bool isRuntime = settings.value(QStringLiteral("Flatpak Ref/IsRuntime")).toBool();
0827     g_autoptr(GError) error = nullptr;
0828 
0829     // If we already added the remote, just go with it
0830     g_autoptr(FlatpakRemote) remote = flatpak_installation_get_remote_by_name(preferredInstallation(), remoteName.toUtf8().constData(), m_cancellable, &error);
0831     g_autofree char *remoteUrl = flatpak_remote_get_url(remote);
0832     if (remote && QString::fromUtf8(remoteUrl) != refurl) {
0833         remote = nullptr;
0834     }
0835     if (remote) {
0836         Q_ASSERT(!m_refreshAppstreamMetadataJobs.contains(remote));
0837         m_refreshAppstreamMetadataJobs.insert(remote);
0838         auto source = integrateRemote(preferredInstallation(), remote);
0839         if (source) {
0840             const QString ref = composeRef(isRuntime, name, branch);
0841             auto searchComponent = [this, stream, source, ref] {
0842                 auto comps = source->componentsByFlatpakId(ref);
0843                 auto resources = kTransform<QVector<StreamResult>>(comps, [this, source](const auto &comp) {
0844                     return resourceForComponent(comp, source);
0845                 });
0846                 Q_EMIT stream->resourcesFound(resources);
0847                 stream->finish();
0848             };
0849             if (source->m_pool) {
0850                 QTimer::singleShot(0, this, searchComponent);
0851             } else {
0852                 connect(this, &FlatpakBackend::initialized, stream, searchComponent);
0853             }
0854             return;
0855         }
0856     }
0857 
0858     AppStream::Component asComponent = fetchComponentFromRemote(settings, m_cancellable);
0859     const QString iconUrl = settings.value(QStringLiteral("Flatpak Ref/Icon")).toString();
0860     if (!iconUrl.isEmpty()) {
0861         AppStream::Icon icon;
0862         icon.setKind(AppStream::Icon::KindRemote);
0863         icon.setUrl(QUrl(iconUrl));
0864         asComponent.addIcon(icon);
0865     }
0866 
0867     auto resource = new FlatpakResource(asComponent, preferredInstallation(), this);
0868     resource->setFlatpakFileType(FlatpakResource::FileFlatpakRef);
0869     resource->setResourceFile(url);
0870     resource->setResourceLocation(QUrl(refurl));
0871     resource->setOrigin(remoteName);
0872     resource->setDisplayOrigin(remote ? QString::fromUtf8(flatpak_remote_get_title(remote)) : QString());
0873     resource->setFlatpakName(name);
0874     resource->setArch(QString::fromUtf8(flatpak_get_default_arch()));
0875     resource->setBranch(branch);
0876     resource->setType(isRuntime ? FlatpakResource::Runtime : FlatpakResource::DesktopApp);
0877 
0878     QUrl runtimeUrl = QUrl(settings.value(QStringLiteral("Flatpak Ref/RuntimeRepo")).toString());
0879     auto refSource = QSharedPointer<FlatpakSource>::create(this, preferredInstallation());
0880     resource->setTemporarySource(refSource);
0881     m_flatpakSources += refSource;
0882     if (!runtimeUrl.isEmpty()) {
0883         // We need to fetch metadata to find information about required runtime
0884         auto fw = new QFutureWatcher<QByteArray>(this);
0885         connect(fw, &QFutureWatcher<QByteArray>::finished, this, [this, resource, fw, runtimeUrl, stream, refSource]() {
0886             fw->deleteLater();
0887             const auto metadata = fw->result();
0888             // Even when we failed to fetch information about runtime we still want to show the application
0889             if (metadata.isEmpty()) {
0890                 onFetchMetadataFinished(resource, metadata);
0891             } else {
0892                 updateAppMetadata(resource, metadata);
0893 
0894                 auto runtime = getRuntimeForApp(resource);
0895                 if (!runtime || (runtime && !runtime->isInstalled())) {
0896                     auto repoStream = new ResultsStream(QLatin1String("FlatpakStream-searchrepo-") + runtimeUrl.toString());
0897                     connect(repoStream, &ResultsStream::resourcesFound, this, [this, resource, stream, refSource](const QVector<StreamResult> &resources) {
0898                         for (auto res : resources) {
0899                             installApplication(res.resource);
0900                         }
0901                         refSource->addResource(resource);
0902                         Q_EMIT stream->resourcesFound({resource});
0903                         stream->finish();
0904                     });
0905 
0906                     auto fetchRemoteResource = new FlatpakFetchRemoteResourceJob(runtimeUrl, repoStream, this);
0907                     fetchRemoteResource->start();
0908                     return;
0909                 } else {
0910                     refSource->addResource(resource);
0911                 }
0912             }
0913             Q_EMIT stream->resourcesFound({resource});
0914             stream->finish();
0915         });
0916         fw->setFuture(QtConcurrent::run(&m_threadPool, &FlatpakRunnables::fetchMetadata, resource, m_cancellable));
0917     } else {
0918         refSource->addResource(resource);
0919         Q_EMIT stream->resourcesFound({resource});
0920         stream->finish();
0921     }
0922 }
0923 
0924 void FlatpakBackend::addSourceFromFlatpakRepo(const QUrl &url, ResultsStream *stream)
0925 {
0926     auto x = qScopeGuard([stream] {
0927         stream->finish();
0928     });
0929     Q_ASSERT(url.isLocalFile());
0930     QSettings settings(url.toLocalFile(), QSettings::NativeFormat);
0931 
0932     const QString gpgKey = settings.value(QStringLiteral("Flatpak Repo/GPGKey")).toString();
0933     const QString title = settings.value(QStringLiteral("Flatpak Repo/Title")).toString();
0934     const QString repoUrl = settings.value(QStringLiteral("Flatpak Repo/Url")).toString();
0935 
0936     if (gpgKey.isEmpty() || title.isEmpty() || repoUrl.isEmpty()) {
0937         return;
0938     }
0939 
0940     if (gpgKey.startsWith(QLatin1String("http://")) || gpgKey.startsWith(QLatin1String("https://"))) {
0941         return;
0942     }
0943 
0944     AppStream::Component asComponent;
0945     asComponent.addUrl(AppStream::Component::UrlKindHomepage, settings.value(QStringLiteral("Flatpak Repo/Homepage")).toString());
0946     asComponent.setSummary(settings.value(QStringLiteral("Flatpak Repo/Comment")).toString());
0947     asComponent.setDescription(settings.value(QStringLiteral("Flatpak Repo/Description")).toString());
0948     asComponent.setName(title);
0949     asComponent.setId(settings.value(QStringLiteral("Flatpak Repo/Title")).toString());
0950 
0951     const QString iconUrl = settings.value(QStringLiteral("Flatpak Repo/Icon")).toString();
0952     if (!iconUrl.isEmpty()) {
0953         AppStream::Icon icon;
0954         icon.setKind(AppStream::Icon::KindRemote);
0955         icon.setUrl(QUrl(iconUrl));
0956         asComponent.addIcon(icon);
0957     }
0958 
0959     auto resource = new FlatpakResource(asComponent, preferredInstallation(), this);
0960     // Use metadata only for stuff which are not common for all resources
0961     resource->addMetadata(QStringLiteral("gpg-key"), gpgKey);
0962     resource->addMetadata(QStringLiteral("repo-url"), repoUrl);
0963     resource->setBranch(settings.value(QStringLiteral("Flatpak Repo/DefaultBranch")).toString());
0964     resource->setFlatpakName(url.fileName().remove(QStringLiteral(".flatpakrepo")));
0965     resource->setType(FlatpakResource::Source);
0966 
0967     g_autoptr(FlatpakRemote) repo =
0968         flatpak_installation_get_remote_by_name(preferredInstallation(), resource->flatpakName().toUtf8().constData(), m_cancellable, nullptr);
0969     if (!repo) {
0970         resource->setState(AbstractResource::State::None);
0971     } else {
0972         resource->setState(AbstractResource::State::Installed);
0973     }
0974 
0975     Q_EMIT stream->resourcesFound({resource});
0976 }
0977 
0978 void FlatpakBackend::loadAppsFromAppstreamData()
0979 {
0980     for (auto installation : std::as_const(m_installations)) {
0981         // Load applications from appstream metadata
0982         if (g_cancellable_is_cancelled(m_cancellable))
0983             break;
0984 
0985         if (!loadAppsFromAppstreamData(installation)) {
0986             qWarning() << "Failed to load packages from appstream data from installation" << installation;
0987         }
0988     }
0989 }
0990 
0991 bool FlatpakBackend::loadAppsFromAppstreamData(FlatpakInstallation *flatpakInstallation)
0992 {
0993     Q_ASSERT(flatpakInstallation);
0994 
0995     GError *error = nullptr;
0996     g_autoptr(GPtrArray) remotes = flatpak_installation_list_remotes(flatpakInstallation, m_cancellable, &error);
0997     if (!remotes) {
0998         qWarning() << "failed to list remotes" << error->message;
0999         return false;
1000     }
1001 
1002     for (uint i = 0; i < remotes->len; i++) {
1003         FlatpakRemote *remote = FLATPAK_REMOTE(g_ptr_array_index(remotes, i));
1004         loadRemote(flatpakInstallation, remote);
1005     }
1006     return true;
1007 }
1008 
1009 void FlatpakBackend::loadRemote(FlatpakInstallation *installation, FlatpakRemote *remote)
1010 {
1011     g_autoptr(GFile) fileTimestamp = flatpak_remote_get_appstream_timestamp(remote, flatpak_get_default_arch());
1012 
1013     Q_ASSERT(!m_refreshAppstreamMetadataJobs.contains(remote));
1014     m_refreshAppstreamMetadataJobs.insert(remote);
1015 
1016     g_autofree char *path_str = g_file_get_path(fileTimestamp);
1017     QFileInfo fileInfo(QFile::decodeName(path_str));
1018     if (!fileInfo.exists() || fileInfo.lastModified().toUTC().secsTo(QDateTime::currentDateTimeUtc()) > 21600) {
1019         // Refresh appstream metadata in case they have never been refreshed or the cache is older than 6 hours
1020         checkForRemoteUpdates(installation, remote);
1021     } else {
1022         auto source = integrateRemote(installation, remote);
1023         Q_ASSERT(findSource(installation, QString::fromUtf8(flatpak_remote_get_name(remote))) == source);
1024     }
1025 }
1026 
1027 void FlatpakBackend::unloadRemote(FlatpakInstallation *installation, FlatpakRemote *remote)
1028 {
1029     acquireFetching(true);
1030     for (auto it = m_flatpakSources.begin(); it != m_flatpakSources.end();) {
1031         if ((*it)->url() == QString::fromUtf8(flatpak_remote_get_url(remote)) && (*it)->installation() == installation) {
1032             qDebug() << "unloading remote" << (*it) << remote;
1033             it = m_flatpakSources.erase(it);
1034         } else {
1035             ++it;
1036         }
1037     }
1038     acquireFetching(false);
1039 }
1040 
1041 void FlatpakBackend::metadataRefreshed(FlatpakRemote *remote)
1042 {
1043     const bool removed = m_refreshAppstreamMetadataJobs.remove(remote);
1044     Q_ASSERT(removed);
1045     if (m_refreshAppstreamMetadataJobs.isEmpty()) {
1046         for (auto installation : std::as_const(m_installations)) {
1047             // Load local updates, comparing current and latest commit
1048             loadLocalUpdates(installation);
1049 
1050             if (g_cancellable_is_cancelled(m_cancellable))
1051                 break;
1052         }
1053     }
1054 }
1055 
1056 void FlatpakBackend::createPool(QSharedPointer<FlatpakSource> source)
1057 {
1058     if (source->m_pool) {
1059         metadataRefreshed(source->remote());
1060         return;
1061     }
1062 
1063     const QString appstreamDirPath = source->appstreamDir();
1064     if (!QFile::exists(appstreamDirPath)) {
1065         qWarning() << "No" << appstreamDirPath << "appstream metadata found for" << source->name();
1066         metadataRefreshed(source->remote());
1067         return;
1068     }
1069 
1070     AppStream::Pool *pool = new AppStream::Pool;
1071     auto fw = new QFutureWatcher<bool>(this);
1072     connect(fw, &QFutureWatcher<bool>::finished, this, [this, fw, pool, source]() {
1073         source->m_pool = pool;
1074         m_flatpakLoadingSources.removeAll(source);
1075         if (fw->result()) {
1076             m_flatpakSources += source;
1077         } else {
1078             qWarning() << "Could not open the AppStream metadata pool" << pool->lastError();
1079         }
1080         metadataRefreshed(source->remote());
1081         acquireFetching(false);
1082         fw->deleteLater();
1083     });
1084     acquireFetching(true);
1085 
1086 #ifdef APPSTREAM_NEW_POOL_API
1087     pool->setLoadStdDataLocations(false);
1088 #if ASQ_CHECK_VERSION(0, 16, 0)
1089     pool->addExtraDataLocation(appstreamDirPath, AppStream::Metadata::FormatStyleCatalog);
1090 #else
1091     pool->addExtraDataLocation(appstreamDirPath, AppStream::Metadata::FormatStyleCollection);
1092 #endif
1093 #else
1094     pool->clearMetadataLocations();
1095     pool->addMetadataLocation(appstreamDirPath);
1096     pool->setFlags(AppStream::Pool::FlagReadCollection);
1097     pool->setCacheFlags(AppStream::Pool::CacheFlagUseUser);
1098 
1099     const QString subdir = flatpak_installation_get_id(source->installation()) + QLatin1Char('/') + source->name();
1100     pool->setCacheLocation(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/flatpak-appstream/" + subdir);
1101     QDir().mkpath(pool->cacheLocation());
1102 #endif
1103 
1104     fw->setFuture(QtConcurrent::run(&m_threadPool, [pool] {
1105         return pool->load();
1106     }));
1107 }
1108 
1109 QSharedPointer<FlatpakSource> FlatpakBackend::integrateRemote(FlatpakInstallation *flatpakInstallation, FlatpakRemote *remote)
1110 {
1111     Q_ASSERT(m_refreshAppstreamMetadataJobs.contains(remote));
1112     m_sources->addRemote(remote, flatpakInstallation);
1113     for (auto source : std::as_const(m_flatpakSources)) {
1114         if (source->url() == QString::fromUtf8(flatpak_remote_get_url(remote)) && source->installation() == flatpakInstallation
1115             && source->name() == QString::fromUtf8(flatpak_remote_get_name(remote))) {
1116             createPool(source);
1117             return source;
1118         }
1119     }
1120     for (auto source : std::as_const(m_flatpakLoadingSources)) {
1121         if (source->url() == QString::fromUtf8(flatpak_remote_get_url(remote)) && source->installation() == flatpakInstallation
1122             && source->name() == QString::fromUtf8(flatpak_remote_get_name(remote))) {
1123             createPool(source);
1124             return source;
1125         }
1126     }
1127 
1128     auto source = QSharedPointer<FlatpakSource>::create(this, flatpakInstallation, remote);
1129     if (!source->isEnabled() || flatpak_remote_get_noenumerate(remote)) {
1130         m_flatpakSources += source;
1131         metadataRefreshed(remote);
1132         return source;
1133     }
1134 
1135     createPool(source);
1136     m_flatpakLoadingSources << source;
1137     return source;
1138 }
1139 
1140 void FlatpakBackend::loadLocalUpdates(FlatpakInstallation *flatpakInstallation)
1141 {
1142     g_autoptr(GError) localError = nullptr;
1143     g_autoptr(GPtrArray) refs = flatpak_installation_list_installed_refs(flatpakInstallation, m_cancellable, &localError);
1144     if (!refs) {
1145         qWarning() << "Failed to get list of installed refs for listing local updates:" << localError->message;
1146         return;
1147     }
1148 
1149     for (uint i = 0; i < refs->len; i++) {
1150         FlatpakInstalledRef *ref = FLATPAK_INSTALLED_REF(g_ptr_array_index(refs, i));
1151 
1152         const gchar *latestCommit = flatpak_installed_ref_get_latest_commit(ref);
1153 
1154         if (!latestCommit) {
1155             qWarning() << "Couldn't get latest commit for" << flatpak_ref_get_name(FLATPAK_REF(ref));
1156             continue;
1157         }
1158 
1159         const gchar *commit = flatpak_ref_get_commit(FLATPAK_REF(ref));
1160         if (g_strcmp0(commit, latestCommit) == 0) {
1161             continue;
1162         }
1163 
1164         FlatpakResource *resource = getAppForInstalledRef(flatpakInstallation, ref);
1165         if (resource) {
1166             resource->setState(AbstractResource::Upgradeable);
1167             updateAppSize(resource);
1168         }
1169         Q_ASSERT(!resource->temporarySource());
1170     }
1171 }
1172 
1173 bool FlatpakBackend::parseMetadataFromAppBundle(FlatpakResource *resource)
1174 {
1175     g_autoptr(GError) localError = nullptr;
1176     g_autoptr(FlatpakRef) ref = flatpak_ref_parse(resource->ref().toUtf8().constData(), &localError);
1177     if (!ref) {
1178         qWarning() << "Failed to parse" << resource->ref() << localError->message;
1179         return false;
1180     } else {
1181         resource->updateFromRef(ref);
1182     }
1183 
1184     return true;
1185 }
1186 
1187 bool FlatpakBackend::setupFlatpakInstallations(GError **error)
1188 {
1189     if (qEnvironmentVariableIsSet("FLATPAK_TEST_MODE")) {
1190         const QString path = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + QLatin1String("/discover-flatpak-test");
1191         qDebug() << "running flatpak backend on test mode" << path;
1192         g_autoptr(GFile) file = g_file_new_for_path(QFile::encodeName(path).constData());
1193         m_installations << flatpak_installation_new_for_path(file, true, m_cancellable, error);
1194         return m_installations.constLast() != nullptr;
1195     }
1196 
1197     g_autoptr(GPtrArray) installations = flatpak_get_system_installations(m_cancellable, error);
1198     if (*error) {
1199         qWarning() << "Failed to call flatpak_get_system_installations:" << (*error)->message;
1200     }
1201     for (uint i = 0; installations && i < installations->len; i++) {
1202         auto installation = FLATPAK_INSTALLATION(g_ptr_array_index(installations, i));
1203         g_object_ref(installation);
1204         m_installations << installation;
1205     }
1206 
1207     auto user = flatpak_installation_new_user(m_cancellable, error);
1208     if (user) {
1209         m_installations << user;
1210     }
1211 
1212     return !m_installations.isEmpty();
1213 }
1214 
1215 void FlatpakBackend::updateAppInstalledMetadata(FlatpakInstalledRef *installedRef, FlatpakResource *resource)
1216 {
1217     // Update the rest
1218     resource->updateFromRef(FLATPAK_REF(installedRef));
1219     resource->setInstalledSize(flatpak_installed_ref_get_installed_size(installedRef));
1220     resource->setOrigin(QString::fromUtf8(flatpak_installed_ref_get_origin(installedRef)));
1221     if (resource->state() < AbstractResource::Installed)
1222         resource->setState(AbstractResource::Installed);
1223 }
1224 
1225 bool FlatpakBackend::updateAppMetadata(FlatpakResource *resource)
1226 {
1227     if (resource->resourceType() != FlatpakResource::DesktopApp) {
1228         return true;
1229     }
1230 
1231     const QString path = resource->installPath() + QStringLiteral("/metadata");
1232 
1233     if (QFile::exists(path)) {
1234         return updateAppMetadata(resource, path);
1235     } else {
1236         auto fw = new QFutureWatcher<QByteArray>(this);
1237         connect(fw, &QFutureWatcher<QByteArray>::finished, this, [this, resource, fw]() {
1238             const auto metadata = fw->result();
1239             if (!metadata.isEmpty())
1240                 onFetchMetadataFinished(resource, metadata);
1241             fw->deleteLater();
1242         });
1243         fw->setFuture(QtConcurrent::run(&m_threadPool, &FlatpakRunnables::fetchMetadata, resource, m_cancellable));
1244 
1245         // Return false to indicate we cannot continue (right now used only in updateAppSize())
1246         return false;
1247     }
1248 }
1249 
1250 void FlatpakBackend::onFetchMetadataFinished(FlatpakResource *resource, const QByteArray &metadata)
1251 {
1252     updateAppMetadata(resource, metadata);
1253 
1254     // Right now we attempt to update metadata for calculating the size so call updateSizeFromRemote()
1255     // as it's what we want. In future if there are other reason to update metadata we will need to somehow
1256     // distinguish between these calls
1257     updateAppSizeFromRemote(resource);
1258 }
1259 
1260 bool FlatpakBackend::updateAppMetadata(FlatpakResource *resource, const QString &path)
1261 {
1262     // Parse the temporary file
1263     QSettings setting(path, QSettings::NativeFormat);
1264     setting.beginGroup(QLatin1String("Application"));
1265     // Set the runtime in form of name/arch/version which can be later easily parsed
1266     resource->setRuntime(setting.value(QLatin1String("runtime")).toString());
1267     // TODO get more information?
1268     return true;
1269 }
1270 
1271 bool FlatpakBackend::updateAppMetadata(FlatpakResource *resource, const QByteArray &data)
1272 {
1273     // We just find the runtime with a regex, QSettings only can read from disk (and so does KConfig)
1274     const QRegularExpression rx(QStringLiteral("runtime=(.*)"));
1275     const auto match = rx.match(QString::fromUtf8(data));
1276     if (!match.isValid()) {
1277         return false;
1278     }
1279 
1280     resource->setRuntime(match.captured(1));
1281     return true;
1282 }
1283 
1284 bool FlatpakBackend::updateAppSize(FlatpakResource *resource)
1285 {
1286     // Check if the size is already set, we should also distinguish between download and installed size,
1287     // right now it doesn't matter whether we get size for installed or not installed app, but if we
1288     // start making difference then for not installed app check download and install size separately
1289 
1290     if (resource->state() == AbstractResource::Installed) {
1291         // The size appears to be already set (from updateAppInstalledMetadata() apparently)
1292         if (resource->installedSize() > 0) {
1293             return true;
1294         }
1295     } else {
1296         if (resource->installedSize() > 0 && resource->downloadSize() > 0) {
1297             return true;
1298         }
1299     }
1300 
1301     // Check if we know the needed runtime which is needed for calculating the size
1302     if (resource->runtime().isEmpty()) {
1303         if (!updateAppMetadata(resource)) {
1304             return false;
1305         }
1306     }
1307 
1308     return updateAppSizeFromRemote(resource);
1309 }
1310 
1311 bool FlatpakBackend::updateAppSizeFromRemote(FlatpakResource *resource)
1312 {
1313     // Calculate the runtime size
1314     if (resource->state() == AbstractResource::None && resource->resourceType() == FlatpakResource::DesktopApp) {
1315         auto runtime = getRuntimeForApp(resource);
1316         if (runtime) {
1317             // Re-check runtime state if case a new one was created
1318             updateAppState(runtime);
1319 
1320             if (!runtime->isInstalled()) {
1321                 if (!updateAppSize(runtime)) {
1322                     qWarning() << "Failed to get runtime size needed for total size of" << resource->name();
1323                     return false;
1324                 }
1325                 // Set required download size to include runtime size even now, in case we fail to
1326                 // get the app size (e.g. when installing bundles where download size is 0)
1327                 resource->setDownloadSize(runtime->downloadSize());
1328             }
1329         }
1330     }
1331 
1332     if (resource->state() == AbstractResource::Installed) {
1333         g_autoptr(FlatpakInstalledRef) ref = nullptr;
1334         ref = getInstalledRefForApp(resource);
1335         if (!ref) {
1336             qWarning() << "Failed to get installed size of" << resource->name();
1337             return false;
1338         }
1339         resource->setInstalledSize(flatpak_installed_ref_get_installed_size(ref));
1340     } else if (resource->resourceType() != FlatpakResource::Source) {
1341         if (resource->origin().isEmpty()) {
1342             qWarning() << "Failed to get size of" << resource->name() << " because of missing origin";
1343             return false;
1344         }
1345 
1346         if (resource->propertyState(FlatpakResource::DownloadSize) == FlatpakResource::Fetching) {
1347             return true;
1348         }
1349 
1350         auto futureWatcher = new QFutureWatcher<FlatpakRemoteRef *>(this);
1351         connect(futureWatcher, &QFutureWatcher<FlatpakRemoteRef *>::finished, this, [this, resource, futureWatcher]() {
1352             g_autoptr(FlatpakRemoteRef) remoteRef = futureWatcher->result();
1353             if (remoteRef) {
1354                 onFetchSizeFinished(resource, flatpak_remote_ref_get_download_size(remoteRef), flatpak_remote_ref_get_installed_size(remoteRef));
1355             } else {
1356                 resource->setPropertyState(FlatpakResource::DownloadSize, FlatpakResource::UnknownOrFailed);
1357                 resource->setPropertyState(FlatpakResource::InstalledSize, FlatpakResource::UnknownOrFailed);
1358             }
1359             futureWatcher->deleteLater();
1360         });
1361         resource->setPropertyState(FlatpakResource::DownloadSize, FlatpakResource::Fetching);
1362         resource->setPropertyState(FlatpakResource::InstalledSize, FlatpakResource::Fetching);
1363 
1364         futureWatcher->setFuture(QtConcurrent::run(&m_threadPool, &FlatpakRunnables::findRemoteRef, resource, m_cancellable));
1365     }
1366 
1367     return true;
1368 }
1369 
1370 void FlatpakBackend::onFetchSizeFinished(FlatpakResource *resource, guint64 downloadSize, guint64 installedSize)
1371 {
1372     FlatpakResource *runtime = nullptr;
1373     if (resource->state() == AbstractResource::None && resource->resourceType() == FlatpakResource::DesktopApp) {
1374         runtime = getRuntimeForApp(resource);
1375     }
1376 
1377     if (runtime && !runtime->isInstalled()) {
1378         resource->setDownloadSize(runtime->downloadSize() + downloadSize);
1379     } else {
1380         resource->setDownloadSize(downloadSize);
1381     }
1382     resource->setInstalledSize(installedSize);
1383 }
1384 
1385 void FlatpakBackend::updateAppState(FlatpakResource *resource)
1386 {
1387     g_autoptr(FlatpakInstalledRef) ref = getInstalledRefForApp(resource);
1388     if (ref) {
1389         // If the app is installed, we can set information about commit, arch etc.
1390         updateAppInstalledMetadata(ref, resource);
1391     } else {
1392         // TODO check if the app is actually still available
1393         resource->setState(AbstractResource::None);
1394     }
1395 }
1396 
1397 void FlatpakBackend::acquireFetching(bool f)
1398 {
1399     if (f)
1400         m_isFetching++;
1401     else
1402         m_isFetching--;
1403 
1404     if ((!f && m_isFetching == 0) || (f && m_isFetching == 1)) {
1405         Q_EMIT fetchingChanged();
1406     }
1407 
1408     if (m_isFetching == 0)
1409         Q_EMIT initialized();
1410 }
1411 
1412 int FlatpakBackend::updatesCount() const
1413 {
1414     return m_updater->updatesCount();
1415 }
1416 
1417 bool FlatpakBackend::flatpakResourceLessThan(const StreamResult &l, const StreamResult &r) const
1418 {
1419     if (l.sortScore == r.sortScore) {
1420         return flatpakResourceLessThan(l.resource, r.resource);
1421     }
1422     return l.sortScore < r.sortScore;
1423 }
1424 
1425 bool FlatpakBackend::flatpakResourceLessThan(AbstractResource *l, AbstractResource *r) const
1426 {
1427     // clang-format off
1428     return (l->isInstalled() != r->isInstalled()) ? l->isInstalled()
1429          : (l->origin() != r->origin()) ? m_sources->originIndex(l->origin()) < m_sources->originIndex(r->origin())
1430          : (l->rating() && r->rating() && l->rating()->ratingPoints() != r->rating()->ratingPoints()) ? l->rating()->ratingPoints() > r->rating()->ratingPoints()
1431          : l < r;
1432     // clang-format on
1433 }
1434 
1435 ResultsStream *FlatpakBackend::deferredResultStream(const QString &streamName, std::function<void(ResultsStream *)> callback)
1436 {
1437     ResultsStream *stream = new ResultsStream(streamName);
1438 
1439     auto f = [stream, callback = std::move(callback)] {
1440         callback(stream);
1441     };
1442 
1443     if (isFetching()) {
1444         connect(this, &FlatpakBackend::initialized, stream, f);
1445     } else {
1446         QTimer::singleShot(0, this, f);
1447     }
1448 
1449     return stream;
1450 }
1451 
1452 ResultsStream *FlatpakBackend::search(const AbstractResourcesBackend::Filters &filter)
1453 {
1454     const auto fileName = filter.resourceUrl.fileName();
1455     if (fileName.endsWith(QLatin1String(".flatpakrepo")) || fileName.endsWith(QLatin1String(".flatpakref")) || fileName.endsWith(QLatin1String(".flatpak"))) {
1456         auto stream = new ResultsStream(QLatin1String("FlatpakStream-http-") + fileName);
1457         FlatpakFetchRemoteResourceJob *fetchResourceJob = new FlatpakFetchRemoteResourceJob(filter.resourceUrl, stream, this);
1458         fetchResourceJob->start();
1459         return stream;
1460     } else if (filter.resourceUrl.scheme() == QLatin1String("appstream")) {
1461         return findResourceByPackageName(filter.resourceUrl);
1462     } else if (!filter.resourceUrl.isEmpty()) {
1463         return new ResultsStream(QStringLiteral("FlatpakStream-void"), {});
1464     } else if (filter.state == AbstractResource::Upgradeable) {
1465         return deferredResultStream(u"FlatpakStream-upgradeable"_s, [this](ResultsStream *stream) {
1466             auto fw = new QFutureWatcher<QHash<FlatpakInstallation *, QVector<FlatpakInstalledRef *>>>(this);
1467             connect(fw, &QFutureWatcher<QHash<FlatpakInstallation *, QVector<FlatpakInstalledRef *>>>::finished, this, [this, fw, stream]() {
1468                 if (g_cancellable_is_cancelled(m_cancellable)) {
1469                     stream->finish();
1470                     fw->deleteLater();
1471                     return;
1472                 }
1473 
1474                 const auto refs = fw->result();
1475                 QVector<StreamResult> resources;
1476                 for (auto it = refs.constBegin(), itEnd = refs.constEnd(); it != itEnd; ++it) {
1477                     resources.reserve(resources.size() + it->size());
1478                     for (auto ref : std::as_const(it.value())) {
1479                         bool fresh;
1480                         auto resource = getAppForInstalledRef(it.key(), ref, &fresh);
1481                         g_object_unref(ref);
1482                         if (resource) {
1483                             resource->setState(AbstractResource::Upgradeable, !fresh);
1484                             updateAppSize(resource);
1485                             if (resource->resourceType() == FlatpakResource::Runtime) {
1486                                 resources.prepend(resource);
1487                             } else {
1488                                 resources.append(resource);
1489                             }
1490                         }
1491                     }
1492                 }
1493 
1494                 if (!resources.isEmpty())
1495                     Q_EMIT stream->resourcesFound(resources);
1496                 stream->finish();
1497                 fw->deleteLater();
1498             });
1499 
1500             QVector<FlatpakInstallation *> installations = m_installations;
1501             auto cancellable = m_cancellable;
1502             fw->setFuture(QtConcurrent::run(&m_threadPool, [installations, cancellable] {
1503                 QHash<FlatpakInstallation *, QVector<FlatpakInstalledRef *>> ret;
1504                 if (g_cancellable_is_cancelled(cancellable)) {
1505                     qWarning() << "Job cancelled";
1506                     return ret;
1507                 }
1508 
1509                 for (auto installation : std::as_const(installations)) {
1510                     g_autoptr(GError) localError = nullptr;
1511                     g_autoptr(GPtrArray) refs = flatpak_installation_list_installed_refs_for_update(installation, cancellable, &localError);
1512                     if (!refs) {
1513                         qWarning() << "Failed to get list of installed refs for listing updates:" << localError->message;
1514                         continue;
1515                     }
1516                     if (g_cancellable_is_cancelled(cancellable)) {
1517                         qWarning() << "Job cancelled";
1518                         ret.clear();
1519                         break;
1520                     }
1521 
1522                     if (refs->len == 0) {
1523                         continue;
1524                     }
1525 
1526                     auto &current = ret[installation];
1527                     current.reserve(refs->len);
1528                     for (uint i = 0; i < refs->len; i++) {
1529                         FlatpakInstalledRef *ref = FLATPAK_INSTALLED_REF(g_ptr_array_index(refs, i));
1530                         g_object_ref(ref);
1531                         current.append(ref);
1532                     }
1533                 }
1534                 return ret;
1535             }));
1536         });
1537     } else if (filter.state == AbstractResource::Installed) {
1538         return deferredResultStream(u"FlatpakStream-installed"_s, [this, filter](ResultsStream *stream) {
1539             QVector<StreamResult> resources;
1540             for (auto installation : std::as_const(m_installations)) {
1541                 g_autoptr(GError) localError = nullptr;
1542                 g_autoptr(GPtrArray) refs = flatpak_installation_list_installed_refs(installation, m_cancellable, &localError);
1543                 if (!refs) {
1544                     qWarning() << "Failed to get list of installed refs for listing installed:" << localError->message;
1545                     continue;
1546                 }
1547 
1548                 resources.reserve(resources.size() + refs->len);
1549                 for (uint i = 0; i < refs->len; i++) {
1550                     FlatpakInstalledRef *ref = FLATPAK_INSTALLED_REF(g_ptr_array_index(refs, i));
1551                     QString name = QString::fromUtf8(flatpak_installed_ref_get_appdata_name(ref));
1552                     if (name.endsWith(QLatin1String(".Debug")) || name.endsWith(QLatin1String(".Locale")) || name.endsWith(QLatin1String(".BaseApp"))
1553                         || name.endsWith(QLatin1String(".Docs")))
1554                         continue;
1555 
1556                     auto resource = getAppForInstalledRef(installation, ref);
1557                     if (!resource) {
1558                         continue;
1559                     }
1560                     if (!filter.search.isEmpty() && !resource->name().contains(filter.search, Qt::CaseInsensitive)
1561                         && !resource->appstreamId().contains(filter.search, Qt::CaseInsensitive))
1562                         continue;
1563                     if (resource->resourceType() == FlatpakResource::Runtime) {
1564                         resources.prepend(resource);
1565                     } else {
1566                         resources.append(resource);
1567                     }
1568                 }
1569             }
1570             if (!resources.isEmpty())
1571                 Q_EMIT stream->resourcesFound(resources);
1572             stream->finish();
1573         });
1574     } else {
1575         return deferredResultStream(u"FlatpakStream"_s, [this, filter](ResultsStream *stream) {
1576             QVector<StreamResult> prioritary, rest;
1577             for (const auto &source : std::as_const(m_flatpakSources)) {
1578                 QList<FlatpakResource *> resources;
1579                 if (source->m_pool) {
1580                     const auto a = !filter.search.isEmpty() ? source->m_pool->search(filter.search)
1581 #if ASQ_CHECK_VERSION(0, 15, 6)
1582                         : filter.category ? AppStreamUtils::componentsByCategories(source->m_pool, filter.category, AppStream::Bundle::KindFlatpak)
1583 #endif
1584                                           : source->m_pool->components();
1585                     resources = kTransform<QList<FlatpakResource *>>(a, [this, &source](const auto &comp) {
1586                         return resourceForComponent(comp, source);
1587                     });
1588                 } else {
1589                     resources = source->m_resources.values();
1590                 }
1591 
1592                 for (auto r : std::as_const(resources)) {
1593                     const bool matchById = r->appstreamId().compare(filter.search, Qt::CaseInsensitive) == 0;
1594                     if (r->type() == AbstractResource::Technical && filter.state != AbstractResource::Upgradeable && !matchById) {
1595                         continue;
1596                     }
1597                     if (r->state() < filter.state)
1598                         continue;
1599 
1600                     if (!filter.extends.isEmpty() && !r->extends().contains(filter.extends))
1601                         continue;
1602 
1603                     if (!filter.mimetype.isEmpty() && !r->mimetypes().contains(filter.mimetype))
1604                         continue;
1605 
1606                     if (filter.search.isEmpty() || matchById) {
1607                         rest += r;
1608                     } else if (r->name().contains(filter.search, Qt::CaseInsensitive)) {
1609                         prioritary += r;
1610                     } else if (r->comment().contains(filter.search, Qt::CaseInsensitive)) {
1611                         rest += r;
1612                         // trust The search terms provided by appstream are relevant, this makes possible finding "gimp"
1613                         // since the name() is "GNU Image Manipulation Program"
1614                     } else if (r->appstreamId().contains(filter.search, Qt::CaseInsensitive)) {
1615                         rest += r;
1616                     }
1617                 }
1618             }
1619             auto f = [this](auto l, auto r) {
1620                 return flatpakResourceLessThan(l, r);
1621             };
1622             std::sort(rest.begin(), rest.end(), f);
1623             std::sort(prioritary.begin(), prioritary.end(), f);
1624             rest = prioritary + rest;
1625             if (!rest.isEmpty())
1626                 Q_EMIT stream->resourcesFound(rest);
1627             stream->finish();
1628         });
1629     }
1630 }
1631 
1632 bool FlatpakBackend::isTracked(FlatpakResource *resource) const
1633 {
1634     const auto uid = resource->uniqueId();
1635     return std::any_of(m_flatpakSources.constBegin(), m_flatpakSources.constEnd(), [uid](const auto &source) {
1636         return source->m_resources.contains(uid);
1637     });
1638 }
1639 
1640 QVector<StreamResult> FlatpakBackend::resultsByAppstreamName(const QString &name) const
1641 {
1642     QVector<StreamResult> resources;
1643     for (const auto &source : m_flatpakSources) {
1644         if (source->m_pool) {
1645             auto comps = source->componentsByName(name);
1646             resources << kTransform<QVector<StreamResult>>(comps, [this, source](const AppStream::Component &comp) -> StreamResult {
1647                 return {resourceForComponent(comp, source), comp.sortScore()};
1648             });
1649         }
1650     }
1651     auto f = [this](auto l, auto r) {
1652         return flatpakResourceLessThan(l, r);
1653     };
1654     std::sort(resources.begin(), resources.end(), f);
1655     return resources;
1656 }
1657 
1658 ResultsStream *FlatpakBackend::findResourceByPackageName(const QUrl &url)
1659 {
1660     if (url.scheme() == QLatin1String("appstream")) {
1661         const auto appstreamIds = AppStreamUtils::appstreamIds(url);
1662         if (appstreamIds.isEmpty())
1663             Q_EMIT passiveMessage(i18n("Malformed appstream url '%1'", url.toDisplayString()));
1664         else {
1665             auto stream = new ResultsStream(QStringLiteral("FlatpakStream-AppStreamUrl"));
1666             auto f = [this, stream, appstreamIds] {
1667                 std::set<AbstractResource *> resources;
1668                 QVector<StreamResult> resourcesVector;
1669                 for (const auto &appstreamId : appstreamIds) {
1670                     const auto resourcesFound = resultsByAppstreamName(appstreamId);
1671                     for (auto res : resourcesFound) {
1672                         auto [x, inserted] = resources.insert(res.resource);
1673                         if (inserted) {
1674                             resourcesVector.append(res);
1675                         }
1676                     }
1677                 }
1678                 if (!resourcesVector.isEmpty())
1679                     Q_EMIT stream->resourcesFound(resourcesVector);
1680                 stream->finish();
1681             };
1682 
1683             if (isFetching()) {
1684                 connect(this, &FlatpakBackend::initialized, stream, f);
1685             } else {
1686                 QTimer::singleShot(0, this, f);
1687             }
1688             return stream;
1689         }
1690     }
1691     return new ResultsStream(QStringLiteral("FlatpakStream-packageName-void"), {});
1692 }
1693 
1694 FlatpakResource *FlatpakBackend::resourceForComponent(const AppStream::Component &component, const QSharedPointer<FlatpakSource> &source) const
1695 {
1696     const auto ref = idForComponent(component);
1697     auto resource = source->m_resources.value(ref);
1698     if (resource) {
1699         return resource;
1700     }
1701 
1702     FlatpakResource *res = new FlatpakResource(component, source->installation(), const_cast<FlatpakBackend *>(this));
1703     res->setOrigin(source->name());
1704     res->setDisplayOrigin(source->title());
1705     res->setIconPath(source->appstreamIconsDir());
1706     res->updateFromAppStream();
1707     source->addResource(res);
1708     Q_ASSERT(ref == res->uniqueId());
1709     return res;
1710 }
1711 
1712 AbstractBackendUpdater *FlatpakBackend::backendUpdater() const
1713 {
1714     return m_updater;
1715 }
1716 
1717 AbstractReviewsBackend *FlatpakBackend::reviewsBackend() const
1718 {
1719     return m_reviews.data();
1720 }
1721 
1722 void FlatpakBackend::checkRepositories(const QMap<QString, QStringList> &names)
1723 {
1724     auto flatpakInstallationByPath = [this](const QString &path) -> FlatpakInstallation * {
1725         for (auto inst : std::as_const(m_installations)) {
1726             if (FlatpakResource::installationPath(inst) == path)
1727                 return inst;
1728         }
1729         return nullptr;
1730     };
1731 
1732     g_autoptr(GError) localError = nullptr;
1733     for (auto it = names.begin(), itEnd = names.end(); it != itEnd; ++it) {
1734         FlatpakInstallation *installation = flatpakInstallationByPath(it.key());
1735         for (const QString &name : std::as_const(*it)) {
1736             auto remote = flatpak_installation_get_remote_by_name(installation, name.toUtf8().constData(), m_cancellable, &localError);
1737             if (!remote) {
1738                 qWarning() << "Could not find remote" << name << "in" << it.key();
1739                 continue;
1740             }
1741             loadRemote(installation, remote);
1742         }
1743     }
1744 }
1745 
1746 FlatpakRemote *FlatpakBackend::installSource(FlatpakResource *resource)
1747 {
1748     g_autoptr(GCancellable) cancellable = g_cancellable_new();
1749 
1750     auto remote = flatpak_installation_get_remote_by_name(preferredInstallation(), resource->flatpakName().toUtf8().constData(), cancellable, nullptr);
1751     if (remote) {
1752         qWarning() << "Source " << resource->flatpakName() << " already exists in" << flatpak_installation_get_path(preferredInstallation());
1753         return nullptr;
1754     }
1755 
1756     remote = flatpak_remote_new(resource->flatpakName().toUtf8().constData());
1757     populateRemote(remote,
1758                    resource->comment(),
1759                    resource->getMetadata(QStringLiteral("repo-url")).toString(),
1760                    resource->getMetadata(QStringLiteral("gpg-key")).toString());
1761     if (!resource->branch().isEmpty()) {
1762         flatpak_remote_set_default_branch(remote, resource->branch().toUtf8().constData());
1763     }
1764 
1765     g_autoptr(GError) error = nullptr;
1766     if (!flatpak_installation_add_remote(preferredInstallation(), remote, false, cancellable, &error)) {
1767         Q_EMIT passiveMessage(i18n("Failed to add source '%1': %2", resource->flatpakName(), QString::fromUtf8(error->message)));
1768         qWarning() << "Failed to add source " << resource->flatpakName() << error->message;
1769         return nullptr;
1770     }
1771     return remote;
1772 }
1773 
1774 Transaction *FlatpakBackend::installApplication(AbstractResource *app, const AddonList &addons)
1775 {
1776     Q_UNUSED(addons);
1777 
1778     FlatpakResource *resource = qobject_cast<FlatpakResource *>(app);
1779 
1780     if (resource->resourceType() == FlatpakResource::Source) {
1781         // Let source backend handle this
1782         FlatpakRemote *remote = installSource(resource);
1783         if (remote) {
1784             resource->setState(AbstractResource::Installed);
1785             auto name = flatpak_remote_get_name(remote);
1786             g_autoptr(FlatpakRemote) remote = flatpak_installation_get_remote_by_name(resource->installation(), name, m_cancellable, nullptr);
1787             loadRemote(resource->installation(), remote);
1788         }
1789         return nullptr;
1790     }
1791 
1792     FlatpakJobTransaction *transaction = new FlatpakJobTransaction(resource, Transaction::InstallRole);
1793     connect(transaction, &FlatpakJobTransaction::repositoriesAdded, this, &FlatpakBackend::checkRepositories);
1794     connect(transaction, &FlatpakJobTransaction::statusChanged, this, [this, resource](Transaction::Status status) {
1795         if (status == Transaction::Status::DoneStatus) {
1796             if (auto tempSource = resource->temporarySource()) {
1797                 auto source = findSource(resource->installation(), resource->origin());
1798                 if (!source) {
1799                     // It could mean that it's still integrating after checkRepositories It should update itself
1800                     return;
1801                 }
1802                 resource->setTemporarySource({});
1803                 const auto id = resource->uniqueId();
1804                 source->m_resources.insert(id, resource);
1805 
1806                 tempSource->m_resources.remove(id);
1807                 if (tempSource->m_resources.isEmpty()) {
1808                     const bool removed = m_flatpakSources.removeAll(tempSource) || m_flatpakLoadingSources.removeAll(tempSource);
1809                     Q_ASSERT(removed);
1810                 }
1811             }
1812             updateAppState(resource);
1813         }
1814     });
1815     return transaction;
1816 }
1817 
1818 Transaction *FlatpakBackend::installApplication(AbstractResource *app)
1819 {
1820     return installApplication(app, {});
1821 }
1822 
1823 Transaction *FlatpakBackend::removeApplication(AbstractResource *app)
1824 {
1825     FlatpakResource *resource = qobject_cast<FlatpakResource *>(app);
1826 
1827     if (resource->resourceType() == FlatpakResource::Source) {
1828         // Let source backend handle this
1829         if (m_sources->removeSource(resource->flatpakName())) {
1830             resource->setState(AbstractResource::None);
1831         }
1832         return nullptr;
1833     }
1834 
1835     FlatpakJobTransaction *transaction = new FlatpakJobTransaction(resource, Transaction::RemoveRole);
1836     connect(transaction, &FlatpakJobTransaction::repositoriesAdded, this, &FlatpakBackend::checkRepositories);
1837     connect(transaction, &FlatpakJobTransaction::statusChanged, this, [this, resource](Transaction::Status status) {
1838         if (status == Transaction::Status::DoneStatus) {
1839             updateAppSize(resource);
1840         }
1841     });
1842     return transaction;
1843 }
1844 
1845 void FlatpakBackend::checkForUpdates()
1846 {
1847     disconnect(this, &FlatpakBackend::initialized, m_checkForUpdatesTimer, qOverload<>(&QTimer::start));
1848     for (const auto &source : std::as_const(m_flatpakSources)) {
1849         if (source->remote()) {
1850             Q_ASSERT(!m_refreshAppstreamMetadataJobs.contains(source->remote()));
1851             m_refreshAppstreamMetadataJobs.insert(source->remote());
1852             checkForRemoteUpdates(source->installation(), source->remote());
1853         }
1854     }
1855 }
1856 
1857 void FlatpakBackend::checkForRemoteUpdates(FlatpakInstallation *installation, FlatpakRemote *remote)
1858 {
1859     Q_ASSERT(remote);
1860     const bool needsIntegration = m_refreshAppstreamMetadataJobs.contains(remote);
1861     if (flatpak_remote_get_disabled(remote) || flatpak_remote_get_noenumerate(remote)) {
1862         if (needsIntegration) {
1863             integrateRemote(installation, remote);
1864         }
1865         return;
1866     }
1867 
1868     FlatpakRefreshAppstreamMetadataJob *job = new FlatpakRefreshAppstreamMetadataJob(installation, remote);
1869     if (needsIntegration) {
1870         connect(job, &FlatpakRefreshAppstreamMetadataJob::jobRefreshAppstreamMetadataFinished, this, &FlatpakBackend::integrateRemote);
1871     }
1872     connect(job, &FlatpakRefreshAppstreamMetadataJob::finished, this, [=] {
1873         acquireFetching(false);
1874     });
1875 
1876     acquireFetching(true);
1877     job->start();
1878 }
1879 
1880 QString FlatpakBackend::displayName() const
1881 {
1882     return QStringLiteral("Flatpak");
1883 }
1884 
1885 InlineMessage *FlatpakBackend::explainDysfunction() const
1886 {
1887     if (m_flatpakSources.isEmpty()) {
1888         return new InlineMessage(InlineMessage::Error, QStringLiteral("emblem-error"), i18n("There are no Flatpak sources."), m_sources->actions());
1889     }
1890     for (const auto &source : m_flatpakSources) {
1891         if (source->m_pool && !source->m_pool->lastError().isEmpty()) {
1892             return new InlineMessage(InlineMessage::Error, QStringLiteral("emblem-error"), i18n("Failed to load \"%1\" source", source->name()));
1893         }
1894     }
1895     return AbstractResourcesBackend::explainDysfunction();
1896 }
1897 
1898 bool FlatpakBackend::extends(const QString &extends) const
1899 {
1900     return std::any_of(m_flatpakSources.constBegin(), m_flatpakSources.constEnd(), [extends](const auto &source) {
1901         return source->m_pool && !source->m_pool->lastError().isEmpty() && !source->m_pool->componentsByExtends(extends).isEmpty();
1902     });
1903 }
1904 
1905 #include "FlatpakBackend.moc"