File indexing completed on 2024-05-19 09:21:24

0001 /*
0002  *   SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez <aleixpol@blue-systems.com>
0003  *
0004  *   SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005  */
0006 
0007 #include "ResourcesModel.h"
0008 
0009 #include "AbstractResource.h"
0010 #include "Category/CategoryModel.h"
0011 #include "Transaction/TransactionModel.h"
0012 #include "libdiscover_debug.h"
0013 #include "resources/AbstractBackendUpdater.h"
0014 #include "resources/AbstractResourcesBackend.h"
0015 #include "utils.h"
0016 #include <DiscoverBackendsFactory.h>
0017 #include <KConfigGroup>
0018 #include <KLocalizedString>
0019 #include <KOSRelease>
0020 #include <KSharedConfig>
0021 #include <QCoreApplication>
0022 #include <QIcon>
0023 #include <QMetaProperty>
0024 #include <QThread>
0025 #include <ReviewsBackend/AbstractReviewsBackend.h>
0026 #include <ReviewsBackend/Rating.h>
0027 #include <Transaction/Transaction.h>
0028 #include <functional>
0029 #include <resources/DiscoverAction.h>
0030 
0031 using namespace Qt::StringLiterals;
0032 
0033 ResourcesModel *ResourcesModel::s_self = nullptr;
0034 
0035 ResourcesModel *ResourcesModel::global()
0036 {
0037     if (!s_self) {
0038         s_self = new ResourcesModel;
0039         s_self->init(true);
0040     }
0041     return s_self;
0042 }
0043 
0044 ResourcesModel::ResourcesModel(QObject *parent)
0045     : QObject(parent)
0046     , m_isFetching(false)
0047     , m_initializingBackendsCount(0)
0048     , m_currentApplicationBackend(nullptr)
0049     , m_allInitializedEmitter(new QTimer(this))
0050     , m_updatesCount(
0051           0,
0052           [this] {
0053               int ret = 0;
0054               for (AbstractResourcesBackend *backend : std::as_const(m_backends)) {
0055                   ret += backend->updatesCount();
0056               }
0057               return ret;
0058           },
0059           [this](int count) {
0060               Q_EMIT updatesCountChanged(count);
0061           })
0062     , m_fetchingUpdatesProgress(
0063           0,
0064           [this] {
0065               if (m_backends.isEmpty())
0066                   return 0;
0067 
0068               int sum = 0;
0069               int weights = 0;
0070               for (auto backend : std::as_const(m_backends)) {
0071                   sum += backend->fetchingUpdatesProgress() * backend->fetchingUpdatesProgressWeight();
0072                   weights += backend->fetchingUpdatesProgressWeight();
0073               }
0074               return sum / weights;
0075           },
0076           [this](int progress) {
0077               Q_EMIT fetchingUpdatesProgressChanged(progress);
0078           })
0079 {
0080     connect(this, &ResourcesModel::allInitialized, this, &ResourcesModel::slotFetching);
0081     connect(this, &ResourcesModel::backendsChanged, this, &ResourcesModel::initApplicationsBackend);
0082 }
0083 
0084 void ResourcesModel::init(bool load)
0085 {
0086     Q_ASSERT(QCoreApplication::instance()->thread() == QThread::currentThread());
0087 
0088     m_allInitializedEmitter->setSingleShot(true);
0089     m_allInitializedEmitter->setInterval(0);
0090     connect(m_allInitializedEmitter, &QTimer::timeout, this, [this]() {
0091         if (m_initializingBackendsCount == 0) {
0092             m_isInitializing = false;
0093             Q_EMIT allInitialized();
0094         }
0095     });
0096 
0097     if (load) {
0098         registerAllBackends();
0099     }
0100 
0101     m_updateAction = new DiscoverAction(this);
0102     m_updateAction->setIconName(QStringLiteral("system-software-update"));
0103     m_updateAction->setText(i18n("Refresh"));
0104     connect(this, &ResourcesModel::fetchingChanged, m_updateAction, [this](bool fetching) {
0105         m_updateAction->setEnabled(!fetching);
0106         m_fetchingUpdatesProgress.reevaluate();
0107     });
0108     connect(m_updateAction, &DiscoverAction::triggered, this, &ResourcesModel::checkForUpdates);
0109 
0110     connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, &QObject::deleteLater);
0111 }
0112 
0113 ResourcesModel::ResourcesModel(const QString &backendName, QObject *parent)
0114     : ResourcesModel(parent)
0115 {
0116     s_self = this;
0117     registerBackendByName(backendName);
0118     init(false);
0119 }
0120 
0121 ResourcesModel::~ResourcesModel()
0122 {
0123     s_self = nullptr;
0124     qDeleteAll(m_backends);
0125 }
0126 
0127 void ResourcesModel::addResourcesBackend(AbstractResourcesBackend *backend)
0128 {
0129     Q_ASSERT(!m_backends.contains(backend));
0130     if (!backend->isValid()) {
0131         qCWarning(LIBDISCOVER_LOG) << "Discarding invalid backend" << backend->name();
0132         CategoryModel::global()->blacklistPlugin(backend->name());
0133         backend->deleteLater();
0134         return;
0135     }
0136 
0137     m_backends += backend;
0138     if (!backend->isFetching()) {
0139         m_updatesCount.reevaluate();
0140     } else {
0141         m_initializingBackendsCount++;
0142     }
0143 
0144     connect(backend, &AbstractResourcesBackend::fetchingChanged, this, &ResourcesModel::callerFetchingChanged);
0145     connect(backend, &AbstractResourcesBackend::allDataChanged, this, &ResourcesModel::updateCaller);
0146     connect(backend, &AbstractResourcesBackend::resourcesChanged, this, &ResourcesModel::resourceDataChanged);
0147     connect(backend, &AbstractResourcesBackend::updatesCountChanged, this, [this] {
0148         m_updatesCount.reevaluate();
0149     });
0150     connect(backend, &AbstractResourcesBackend::fetchingUpdatesProgressChanged, this, [this] {
0151         m_fetchingUpdatesProgress.reevaluate();
0152     });
0153     connect(backend, &AbstractResourcesBackend::resourceRemoved, this, &ResourcesModel::resourceRemoved);
0154     connect(backend, &AbstractResourcesBackend::passiveMessage, this, &ResourcesModel::passiveMessage);
0155     connect(backend, &AbstractResourcesBackend::inlineMessageChanged, this, &ResourcesModel::setInlineMessage);
0156     connect(backend->backendUpdater(), &AbstractBackendUpdater::progressingChanged, this, &ResourcesModel::slotFetching);
0157     if (backend->reviewsBackend()) {
0158         connect(backend->reviewsBackend(), &AbstractReviewsBackend::error, this, &ResourcesModel::passiveMessage, Qt::UniqueConnection);
0159     }
0160 
0161     // In case this is in fact the first backend to be added, and also happens to be
0162     // pre-filled, we still need for the rest of the backends to be added before trying
0163     // to send out the initialized signal. To ensure this happens, schedule it for the
0164     // start of the next run of the event loop.
0165     if (m_initializingBackendsCount == 0) {
0166         m_allInitializedEmitter->start();
0167     } else {
0168         slotFetching();
0169     }
0170 }
0171 
0172 void ResourcesModel::callerFetchingChanged()
0173 {
0174     AbstractResourcesBackend *backend = qobject_cast<AbstractResourcesBackend *>(sender());
0175 
0176     if (!backend->isValid()) {
0177         qCWarning(LIBDISCOVER_LOG) << "Discarding invalid backend" << backend->name();
0178         int idx = m_backends.indexOf(backend);
0179         Q_ASSERT(idx >= 0);
0180         m_backends.removeAt(idx);
0181         Q_EMIT backendsChanged();
0182         CategoryModel::global()->blacklistPlugin(backend->name());
0183         backend->deleteLater();
0184         slotFetching();
0185         return;
0186     }
0187 
0188     if (backend->isFetching()) {
0189         m_initializingBackendsCount++;
0190         slotFetching();
0191     } else {
0192         m_initializingBackendsCount--;
0193         if (m_initializingBackendsCount == 0)
0194             m_allInitializedEmitter->start();
0195         else
0196             slotFetching();
0197     }
0198 }
0199 
0200 void ResourcesModel::updateCaller(const QVector<QByteArray> &properties)
0201 {
0202     AbstractResourcesBackend *backend = qobject_cast<AbstractResourcesBackend *>(sender());
0203 
0204     Q_EMIT backendDataChanged(backend, properties);
0205 }
0206 
0207 QVector<AbstractResourcesBackend *> ResourcesModel::backends() const
0208 {
0209     return m_backends;
0210 }
0211 
0212 bool ResourcesModel::hasSecurityUpdates() const
0213 {
0214     bool ret = false;
0215 
0216     for (AbstractResourcesBackend *backend : std::as_const(m_backends)) {
0217         ret |= backend->hasSecurityUpdates();
0218     }
0219 
0220     return ret;
0221 }
0222 
0223 void ResourcesModel::installApplication(AbstractResource *app)
0224 {
0225     TransactionModel::global()->addTransaction(app->backend()->installApplication(app));
0226 }
0227 
0228 void ResourcesModel::installApplication(AbstractResource *app, const AddonList &addons)
0229 {
0230     TransactionModel::global()->addTransaction(app->backend()->installApplication(app, addons));
0231 }
0232 
0233 void ResourcesModel::removeApplication(AbstractResource *app)
0234 {
0235     TransactionModel::global()->addTransaction(app->backend()->removeApplication(app));
0236 }
0237 
0238 void ResourcesModel::registerAllBackends()
0239 {
0240     DiscoverBackendsFactory f;
0241     const auto backends = f.allBackends();
0242     if (m_initializingBackendsCount == 0 && backends.isEmpty()) {
0243         qCWarning(LIBDISCOVER_LOG) << "Couldn't find any backends";
0244         m_allInitializedEmitter->start();
0245     } else {
0246         for (AbstractResourcesBackend *b : backends) {
0247             addResourcesBackend(b);
0248         }
0249         Q_EMIT backendsChanged();
0250     }
0251 }
0252 
0253 void ResourcesModel::registerBackendByName(const QString &name)
0254 {
0255     DiscoverBackendsFactory f;
0256     const auto backends = f.backend(name);
0257     for (auto b : backends)
0258         addResourcesBackend(b);
0259 
0260     Q_EMIT backendsChanged();
0261 }
0262 
0263 bool ResourcesModel::isFetching() const
0264 {
0265     return m_isFetching;
0266 }
0267 
0268 bool ResourcesModel::isInitializing() const
0269 {
0270     return m_isInitializing;
0271 }
0272 
0273 void ResourcesModel::slotFetching()
0274 {
0275     bool newFetching = false;
0276     for (AbstractResourcesBackend *b : std::as_const(m_backends)) {
0277         // isFetching should sort of be enough. However, sometimes the backend itself
0278         // will still be operating on things, which from a model point of view would
0279         // still mean something going on. So, interpret that as fetching as well, for
0280         // the purposes of this property.
0281         if (b->isFetching() || (b->backendUpdater() && b->backendUpdater()->isProgressing())) {
0282             newFetching = true;
0283             break;
0284         }
0285     }
0286     if (newFetching != m_isFetching) {
0287         m_isFetching = newFetching;
0288         Q_EMIT fetchingChanged(m_isFetching);
0289     }
0290 }
0291 
0292 bool ResourcesModel::isBusy() const
0293 {
0294     return TransactionModel::global()->rowCount() > 0;
0295 }
0296 
0297 bool ResourcesModel::isExtended(const QString &id)
0298 {
0299     bool ret = true;
0300     for (AbstractResourcesBackend *backend : std::as_const(m_backends)) {
0301         ret = backend->extends(id);
0302         if (ret)
0303             break;
0304     }
0305     return ret;
0306 }
0307 
0308 AggregatedResultsStream::AggregatedResultsStream(const QSet<ResultsStream *> &streams)
0309     : ResultsStream(QStringLiteral("AggregatedResultsStream"))
0310 {
0311     Q_ASSERT(!streams.contains(nullptr));
0312     if (streams.isEmpty()) {
0313         qCWarning(LIBDISCOVER_LOG) << "no streams to aggregate!!";
0314         QTimer::singleShot(0, this, &AggregatedResultsStream::clear);
0315     }
0316 
0317     for (auto stream : streams) {
0318         connect(stream, &ResultsStream::resourcesFound, this, &AggregatedResultsStream::addResults);
0319         connect(stream, &QObject::destroyed, this, &AggregatedResultsStream::streamDestruction);
0320         connect(this, &ResultsStream::fetchMore, stream, &ResultsStream::fetchMore);
0321         m_streams << stream;
0322     }
0323 
0324     m_delayedEmission.setInterval(0);
0325     connect(&m_delayedEmission, &QTimer::timeout, this, &AggregatedResultsStream::emitResults);
0326 }
0327 
0328 AggregatedResultsStream::~AggregatedResultsStream() = default;
0329 
0330 void AggregatedResultsStream::addResults(const QVector<StreamResult> &res)
0331 {
0332     for (auto r : res)
0333         connect(r.resource, &QObject::destroyed, this, &AggregatedResultsStream::resourceDestruction);
0334 
0335     m_results += res;
0336 
0337     m_delayedEmission.start();
0338 }
0339 
0340 void AggregatedResultsStream::emitResults()
0341 {
0342     if (!m_results.isEmpty()) {
0343         Q_EMIT resourcesFound(m_results);
0344         m_results.clear();
0345     }
0346     m_delayedEmission.setInterval(m_delayedEmission.interval() + 100);
0347     m_delayedEmission.stop();
0348 }
0349 
0350 void AggregatedResultsStream::resourceDestruction(QObject *obj)
0351 {
0352     for (auto it = m_results.begin(); it != m_results.end(); ++it) {
0353         if (obj == it->resource)
0354             it = m_results.erase(it);
0355         else
0356             ++it;
0357     }
0358 }
0359 
0360 void AggregatedResultsStream::streamDestruction(QObject *obj)
0361 {
0362     m_streams.remove(obj);
0363     clear();
0364 }
0365 
0366 void AggregatedResultsStream::clear()
0367 {
0368     if (m_streams.isEmpty()) {
0369         emitResults();
0370         Q_EMIT finished();
0371         deleteLater();
0372     }
0373 }
0374 
0375 AggregatedResultsStream *ResourcesModel::search(const AbstractResourcesBackend::Filters &search)
0376 {
0377     if (search.isEmpty()) {
0378         return new AggregatedResultsStream({new ResultsStream(QStringLiteral("emptysearch"), {})});
0379     }
0380 
0381     auto streams = kTransform<QSet<ResultsStream *>>(m_backends, [search](AbstractResourcesBackend *backend) {
0382         return backend->search(search);
0383     });
0384     return new AggregatedResultsStream(streams);
0385 }
0386 
0387 void ResourcesModel::checkForUpdates()
0388 {
0389     for (auto backend : std::as_const(m_backends))
0390         backend->checkForUpdates();
0391 }
0392 
0393 AbstractResourcesBackend *ResourcesModel::currentApplicationBackend() const
0394 {
0395     return m_currentApplicationBackend;
0396 }
0397 
0398 void ResourcesModel::setCurrentApplicationBackend(AbstractResourcesBackend *backend, bool write)
0399 {
0400     if (backend != m_currentApplicationBackend) {
0401         if (write) {
0402             KConfigGroup settings(KSharedConfig::openConfig(), u"ResourcesModel"_s);
0403             if (backend)
0404                 settings.writeEntry("currentApplicationBackend", backend->name());
0405             else
0406                 settings.deleteEntry("currentApplicationBackend");
0407         }
0408 
0409         qCDebug(LIBDISCOVER_LOG) << "setting currentApplicationBackend" << backend;
0410         m_currentApplicationBackend = backend;
0411         Q_EMIT currentApplicationBackendChanged(backend);
0412     }
0413 }
0414 
0415 void ResourcesModel::initApplicationsBackend()
0416 {
0417     const auto name = applicationSourceName();
0418 
0419     auto idx = kIndexOf(m_backends, [name](AbstractResourcesBackend *b) {
0420         return b->hasApplications() && b->name() == name;
0421     });
0422     if (idx < 0) {
0423         idx = kIndexOf(m_backends, [](AbstractResourcesBackend *b) {
0424             return b->hasApplications();
0425         });
0426         qCDebug(LIBDISCOVER_LOG) << "falling back applications backend to" << idx;
0427     }
0428     setCurrentApplicationBackend(m_backends.value(idx, nullptr), false);
0429 }
0430 
0431 QString ResourcesModel::applicationSourceName() const
0432 {
0433     KConfigGroup settings(KSharedConfig::openConfig(), u"ResourcesModel"_s);
0434     return settings.readEntry<QString>("currentApplicationBackend", QStringLiteral("packagekit-backend"));
0435 }
0436 
0437 QString ResourcesModel::distroName() const
0438 {
0439     return KOSRelease().name();
0440 }
0441 
0442 QUrl ResourcesModel::distroBugReportUrl()
0443 {
0444     return QUrl(KOSRelease().bugReportUrl());
0445 }
0446 
0447 void ResourcesModel::setInlineMessage(const QSharedPointer<InlineMessage> &inlineMessage)
0448 {
0449     if (inlineMessage == m_inlineMessage) {
0450         return;
0451     }
0452 
0453     m_inlineMessage = inlineMessage;
0454     Q_EMIT inlineMessageChanged(inlineMessage);
0455 }