File indexing completed on 2024-05-05 17:33:21

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