File indexing completed on 2024-04-21 03:56:25

0001 /*
0002     SPDX-FileCopyrightText: 2016 Dan Leinir Turthra Jensen <admin@leinir.dk>
0003 
0004     SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0005 */
0006 
0007 #include "quickengine.h"
0008 #include "cache.h"
0009 #include "errorcode.h"
0010 #include "imageloader_p.h"
0011 #include "installation_p.h"
0012 #include "knewstuffquick_debug.h"
0013 #include "quicksettings.h"
0014 
0015 #include <KLocalizedString>
0016 #include <QTimer>
0017 
0018 #include "categoriesmodel.h"
0019 #include "quickquestionlistener.h"
0020 #include "searchpresetmodel.h"
0021 
0022 class EnginePrivate
0023 {
0024 public:
0025     bool isValid = false;
0026     CategoriesModel *categoriesModel = nullptr;
0027     SearchPresetModel *searchPresetModel = nullptr;
0028     QString configFile;
0029     QTimer searchTimer;
0030     Engine::BusyState busyState;
0031     QString busyMessage;
0032     // the current request from providers
0033     KNSCore::Provider::SearchRequest currentRequest;
0034     KNSCore::Provider::SearchRequest storedRequest;
0035     // the page that is currently displayed, so it is not requested repeatedly
0036     int currentPage = -1;
0037 
0038     // when requesting entries from a provider, how many to ask for
0039     int pageSize = 20;
0040 
0041     int numDataJobs = 0;
0042     int numPictureJobs = 0;
0043     int numInstallJobs = 0;
0044 };
0045 
0046 Engine::Engine(QObject *parent)
0047     : KNSCore::EngineBase(parent)
0048     , d(new EnginePrivate)
0049 {
0050     const auto setBusy = [this](Engine::BusyState state, const QString &msg) {
0051         setBusyState(state);
0052         d->busyMessage = msg;
0053     };
0054     setBusy(BusyOperation::Initializing, i18n("Loading data")); // For the user this should be the same as initializing
0055 
0056     KNewStuffQuick::QuickQuestionListener::instance();
0057     d->categoriesModel = new CategoriesModel(this);
0058     connect(d->categoriesModel, &QAbstractListModel::modelReset, this, &Engine::categoriesChanged);
0059     d->searchPresetModel = new SearchPresetModel(this);
0060     connect(d->searchPresetModel, &QAbstractListModel::modelReset, this, &Engine::searchPresetModelChanged);
0061 
0062     d->searchTimer.setSingleShot(true);
0063     d->searchTimer.setInterval(1000);
0064     connect(&d->searchTimer, &QTimer::timeout, this, &Engine::reloadEntries);
0065     connect(installation(), &KNSCore::Installation::signalInstallationFinished, this, [this]() {
0066         --d->numInstallJobs;
0067         updateStatus();
0068     });
0069     connect(installation(), &KNSCore::Installation::signalInstallationFailed, this, [this](const QString &message) {
0070         --d->numInstallJobs;
0071         Q_EMIT signalErrorCode(KNSCore::ErrorCode::InstallationError, message, QVariant());
0072     });
0073     connect(this, &EngineBase::signalProvidersLoaded, this, &Engine::updateStatus);
0074     connect(this, &EngineBase::signalProvidersLoaded, this, [this]() {
0075         d->currentRequest.categories = EngineBase::categories();
0076     });
0077 
0078     connect(this,
0079             &KNSCore::EngineBase::signalErrorCode,
0080             this,
0081             [setBusy, this](const KNSCore::ErrorCode::ErrorCode &error, const QString &message, const QVariant &metadata) {
0082                 Q_EMIT errorCode(error, message, metadata);
0083                 if (error == KNSCore::ErrorCode::ProviderError || error == KNSCore::ErrorCode::ConfigFileError) {
0084                     // This means loading the config or providers file failed entirely and we cannot complete the
0085                     // initialisation. It also means the engine is done loading, but that nothing will
0086                     // work, and we need to inform the user of this.
0087                     setBusy({}, QString());
0088                 }
0089 
0090                 // Emit the signal later, currently QML is not connected to the slot
0091                 if (error == KNSCore::ErrorCode::ConfigFileError) {
0092                     QTimer::singleShot(0, [this, error, message, metadata]() {
0093                         Q_EMIT errorCode(error, message, metadata);
0094                     });
0095                 }
0096             });
0097 
0098     connect(this, &Engine::signalEntryEvent, this, [this](const KNSCore::Entry &entry, KNSCore::Entry::EntryEvent event) {
0099         // Just forward the event but not do anything more
0100         if (event != KNSCore::Entry::StatusChangedEvent) {
0101             Q_EMIT entryEvent(entry, event);
0102             return;
0103         }
0104 
0105         // We do not want to emit the entries changed signal for intermediate changed
0106         // this would cause the KCMs to reload their view unnecessarily, BUG: 431568
0107         if (entry.status() == KNSCore::Entry::Installing || entry.status() == KNSCore::Entry::Updating) {
0108             return;
0109         }
0110         Q_EMIT entryEvent(entry, event);
0111     });
0112     //
0113     // And finally, let's just make sure we don't miss out the various things here getting changed
0114     // In other words, when we're asked to reset the view, actually do that
0115     connect(this, &Engine::signalResetView, this, &Engine::categoriesFilterChanged);
0116     connect(this, &Engine::signalResetView, this, &Engine::filterChanged);
0117     connect(this, &Engine::signalResetView, this, &Engine::sortOrderChanged);
0118     connect(this, &Engine::signalResetView, this, &Engine::searchTermChanged);
0119 }
0120 
0121 bool Engine::init(const QString &configfile)
0122 {
0123     const bool valid = EngineBase::init(configfile);
0124     if (valid) {
0125         connect(this, &Engine::signalEntryEvent, cache().data(), [this](const KNSCore::Entry &entry, KNSCore::Entry::EntryEvent event) {
0126             if (event == KNSCore::Entry::StatusChangedEvent) {
0127                 cache()->registerChangedEntry(entry);
0128             }
0129         });
0130         const auto slotEntryChanged = [this](const KNSCore::Entry &entry) {
0131             Q_EMIT signalEntryEvent(entry, KNSCore::Entry::StatusChangedEvent);
0132         };
0133         connect(installation(), &KNSCore::Installation::signalEntryChanged, this, slotEntryChanged);
0134         connect(cache().data(), &KNSCore::Cache::entryChanged, this, slotEntryChanged);
0135     }
0136     return valid;
0137 }
0138 void Engine::updateStatus()
0139 {
0140     QString busyMessage;
0141     BusyState state;
0142     if (d->numPictureJobs > 0) {
0143         // If it is loading previews or data is irrelevant for the user
0144         busyMessage = i18n("Loading data");
0145         state |= BusyOperation::LoadingPreview;
0146     }
0147     if (d->numInstallJobs > 0) {
0148         busyMessage = i18n("Installing");
0149         state |= BusyOperation::InstallingEntry;
0150     }
0151     if (d->numDataJobs > 0) {
0152         busyMessage = i18n("Loading data");
0153         state |= BusyOperation::LoadingData;
0154     }
0155     d->busyMessage = busyMessage;
0156     setBusyState(state);
0157 }
0158 
0159 bool Engine::needsLazyLoadSpinner()
0160 {
0161     return d->numDataJobs > 0 || d->numPictureJobs;
0162 }
0163 
0164 Engine::~Engine() = default;
0165 
0166 void Engine::setBusyState(BusyState state)
0167 {
0168     d->busyState = state;
0169     Q_EMIT busyStateChanged();
0170 }
0171 Engine::BusyState Engine::busyState() const
0172 {
0173     return d->busyState;
0174 }
0175 QString Engine::busyMessage() const
0176 {
0177     return d->busyMessage;
0178 }
0179 
0180 QString Engine::configFile() const
0181 {
0182     return d->configFile;
0183 }
0184 
0185 void Engine::setConfigFile(const QString &newFile)
0186 {
0187     if (d->configFile != newFile) {
0188         d->configFile = newFile;
0189         Q_EMIT configFileChanged();
0190 
0191         if (KNewStuffQuick::Settings::instance()->allowedByKiosk()) {
0192             d->isValid = init(newFile);
0193             Q_EMIT categoriesFilterChanged();
0194             Q_EMIT filterChanged();
0195             Q_EMIT sortOrderChanged();
0196             Q_EMIT searchTermChanged();
0197         } else {
0198             // This is not an error message in the proper sense, and the message is not intended to look like an error (as there is really
0199             // nothing the user can do to fix it, and we just tell them so they're not wondering what's wrong)
0200             Q_EMIT errorCode(
0201                 KNSCore::ErrorCode::ConfigFileError,
0202                 i18nc("An informational message which is shown to inform the user they are not authorized to use GetHotNewStuff functionality",
0203                       "You are not authorized to Get Hot New Stuff. If you think this is in error, please contact the person in charge of your permissions."),
0204                 QVariant());
0205         }
0206     }
0207 }
0208 
0209 QObject *Engine::categories() const
0210 {
0211     return d->categoriesModel;
0212 }
0213 
0214 QStringList Engine::categoriesFilter() const
0215 {
0216     return d->currentRequest.categories;
0217 }
0218 
0219 void Engine::setCategoriesFilter(const QStringList &newCategoriesFilter)
0220 {
0221     if (d->currentRequest.categories != newCategoriesFilter) {
0222         d->currentRequest.categories = newCategoriesFilter;
0223         reloadEntries();
0224         Q_EMIT categoriesFilterChanged();
0225     }
0226 }
0227 
0228 KNSCore::Provider::Filter Engine::filter() const
0229 {
0230     return d->currentRequest.filter;
0231 }
0232 
0233 void Engine::setFilter(KNSCore::Provider::Filter newFilter)
0234 {
0235     if (d->currentRequest.filter != newFilter) {
0236         d->currentRequest.filter = newFilter;
0237         reloadEntries();
0238         Q_EMIT filterChanged();
0239     }
0240 }
0241 
0242 KNSCore::Provider::SortMode Engine::sortOrder() const
0243 {
0244     return d->currentRequest.sortMode;
0245 }
0246 
0247 void Engine::setSortOrder(KNSCore::Provider::SortMode mode)
0248 {
0249     if (d->currentRequest.sortMode != mode) {
0250         d->currentRequest.sortMode = mode;
0251         reloadEntries();
0252         Q_EMIT sortOrderChanged();
0253     }
0254 }
0255 
0256 QString Engine::searchTerm() const
0257 {
0258     return d->currentRequest.searchTerm;
0259 }
0260 
0261 void Engine::setSearchTerm(const QString &searchTerm)
0262 {
0263     if (d->isValid && d->currentRequest.searchTerm != searchTerm) {
0264         d->currentRequest.searchTerm = searchTerm;
0265         Q_EMIT searchTermChanged();
0266     }
0267     KNSCore::Entry::List cacheEntries = cache()->requestFromCache(d->currentRequest);
0268     if (!cacheEntries.isEmpty()) {
0269         reloadEntries();
0270     } else {
0271         d->searchTimer.start();
0272     }
0273 }
0274 
0275 QObject *Engine::searchPresetModel() const
0276 {
0277     return d->searchPresetModel;
0278 }
0279 
0280 bool Engine::isValid()
0281 {
0282     return d->isValid;
0283 }
0284 
0285 void Engine::updateEntryContents(const KNSCore::Entry &entry)
0286 {
0287     const auto provider = EngineBase::provider(entry.providerId());
0288     if (provider.isNull() || !provider->isInitialized()) {
0289         qCWarning(KNEWSTUFFQUICK) << "Provider was not found or is not initialized" << provider << entry.providerId();
0290         return;
0291     }
0292     provider->loadEntryDetails(entry);
0293 }
0294 
0295 void Engine::reloadEntries()
0296 {
0297     Q_EMIT signalResetView();
0298     d->currentPage = -1;
0299     d->currentRequest.page = 0;
0300     d->numDataJobs = 0;
0301 
0302     const auto providersList = EngineBase::providers();
0303     for (const QSharedPointer<KNSCore::Provider> &p : providersList) {
0304         if (p->isInitialized()) {
0305             if (d->currentRequest.filter == KNSCore::Provider::Installed) {
0306                 // when asking for installed entries, never use the cache
0307                 p->loadEntries(d->currentRequest);
0308             } else {
0309                 // take entries from cache until there are no more
0310                 KNSCore::Entry::List cacheEntries;
0311                 KNSCore::Entry::List lastCache = cache()->requestFromCache(d->currentRequest);
0312                 while (!lastCache.isEmpty()) {
0313                     qCDebug(KNEWSTUFFQUICK) << "From cache";
0314                     cacheEntries << lastCache;
0315 
0316                     d->currentPage = d->currentRequest.page;
0317                     ++d->currentRequest.page;
0318                     lastCache = cache()->requestFromCache(d->currentRequest);
0319                 }
0320 
0321                 // Since the cache has no more pages, reset the request's page
0322                 if (d->currentPage >= 0) {
0323                     d->currentRequest.page = d->currentPage;
0324                 }
0325 
0326                 if (!cacheEntries.isEmpty()) {
0327                     Q_EMIT signalEntriesLoaded(cacheEntries);
0328                 } else {
0329                     qCDebug(KNEWSTUFFQUICK) << "From provider";
0330                     p->loadEntries(d->currentRequest);
0331 
0332                     ++d->numDataJobs;
0333                     updateStatus();
0334                 }
0335             }
0336         }
0337     }
0338 }
0339 void Engine::addProvider(QSharedPointer<KNSCore::Provider> provider)
0340 {
0341     EngineBase::addProvider(provider);
0342     connect(provider.data(), &KNSCore::Provider::loadingFinished, this, [this](const auto &request, const auto &entries) {
0343         d->currentPage = qMax<int>(request.page, d->currentPage);
0344         qCDebug(KNEWSTUFFQUICK) << "loaded page " << request.page << "current page" << d->currentPage << "count:" << entries.count();
0345 
0346         if (request.filter != KNSCore::Provider::Updates) {
0347             cache()->insertRequest(request, entries);
0348         }
0349         Q_EMIT signalEntriesLoaded(entries);
0350 
0351         --d->numDataJobs;
0352         updateStatus();
0353     });
0354     connect(provider.data(), &KNSCore::Provider::entryDetailsLoaded, this, [this](const auto &entry) {
0355         --d->numDataJobs;
0356         updateStatus();
0357         Q_EMIT signalEntryEvent(entry, KNSCore::Entry::DetailsLoadedEvent);
0358     });
0359 }
0360 
0361 void Engine::loadPreview(const KNSCore::Entry &entry, KNSCore::Entry::PreviewType type)
0362 {
0363     qCDebug(KNEWSTUFFQUICK) << "START  preview: " << entry.name() << type;
0364     auto l = new KNSCore::ImageLoader(entry, type, this);
0365     connect(l, &KNSCore::ImageLoader::signalPreviewLoaded, this, [this](const KNSCore::Entry &entry, KNSCore::Entry::PreviewType type) {
0366         qCDebug(KNEWSTUFFQUICK) << "FINISH preview: " << entry.name() << type;
0367         Q_EMIT signalEntryPreviewLoaded(entry, type);
0368         --d->numPictureJobs;
0369         updateStatus();
0370     });
0371     connect(l, &KNSCore::ImageLoader::signalError, this, [this](const KNSCore::Entry &entry, KNSCore::Entry::PreviewType type, const QString &errorText) {
0372         Q_EMIT signalErrorCode(KNSCore::ErrorCode::ImageError, errorText, QVariantList() << entry.name() << type);
0373         qCDebug(KNEWSTUFFQUICK) << "ERROR preview: " << errorText << entry.name() << type;
0374         --d->numPictureJobs;
0375         updateStatus();
0376     });
0377     l->start();
0378     ++d->numPictureJobs;
0379     updateStatus();
0380 }
0381 
0382 void Engine::adoptEntry(const KNSCore::Entry &entry)
0383 {
0384     registerTransaction(KNSCore::Transaction::adopt(this, entry));
0385 }
0386 void Engine::install(const KNSCore::Entry &entry, int linkId)
0387 {
0388     auto transaction = KNSCore::Transaction::install(this, entry, linkId);
0389     registerTransaction(transaction);
0390     if (!transaction->isFinished()) {
0391         ++d->numInstallJobs;
0392     }
0393 }
0394 void Engine::uninstall(const KNSCore::Entry &entry)
0395 {
0396     registerTransaction(KNSCore::Transaction::uninstall(this, entry));
0397 }
0398 void Engine::registerTransaction(KNSCore::Transaction *transaction)
0399 {
0400     connect(transaction, &KNSCore::Transaction::signalErrorCode, this, &EngineBase::signalErrorCode);
0401     connect(transaction, &KNSCore::Transaction::signalMessage, this, &EngineBase::signalMessage);
0402     connect(transaction, &KNSCore::Transaction::signalEntryEvent, this, &Engine::signalEntryEvent);
0403 }
0404 
0405 void Engine::requestMoreData()
0406 {
0407     qCDebug(KNEWSTUFFQUICK) << "Get more data! current page: " << d->currentPage << " requested: " << d->currentRequest.page;
0408 
0409     if (d->currentPage < d->currentRequest.page) {
0410         return;
0411     }
0412 
0413     d->currentRequest.page++;
0414     doRequest();
0415 }
0416 void Engine::doRequest()
0417 {
0418     const auto providersList = providers();
0419     for (const QSharedPointer<KNSCore::Provider> &p : providersList) {
0420         if (p->isInitialized()) {
0421             p->loadEntries(d->currentRequest);
0422             ++d->numDataJobs;
0423             updateStatus();
0424         }
0425     }
0426 }
0427 
0428 void Engine::revalidateCacheEntries()
0429 {
0430     // This gets called from QML, because in QtQuick we reuse the engine, BUG: 417985
0431     // We can't handle this in the cache, because it can't access the configuration of the engine
0432     if (cache()) {
0433         const auto providersList = providers();
0434         for (const auto &provider : providersList) {
0435             if (provider && provider->isInitialized()) {
0436                 const KNSCore::Entry::List cacheBefore = cache()->registryForProvider(provider->id());
0437                 cache()->removeDeletedEntries();
0438                 const KNSCore::Entry::List cacheAfter = cache()->registryForProvider(provider->id());
0439                 // If the user has deleted them in the background we have to update the state to deleted
0440                 for (const auto &oldCachedEntry : cacheBefore) {
0441                     if (!cacheAfter.contains(oldCachedEntry)) {
0442                         KNSCore::Entry removedEntry = oldCachedEntry;
0443                         removedEntry.setEntryDeleted();
0444                         Q_EMIT signalEntryEvent(removedEntry, KNSCore::Entry::StatusChangedEvent);
0445                     }
0446                 }
0447             }
0448         }
0449     }
0450 }
0451 
0452 void Engine::restoreSearch()
0453 {
0454     d->searchTimer.stop();
0455     d->currentRequest = d->storedRequest;
0456     if (cache()) {
0457         KNSCore::Entry::List cacheEntries = cache()->requestFromCache(d->currentRequest);
0458         if (!cacheEntries.isEmpty()) {
0459             reloadEntries();
0460         } else {
0461             d->searchTimer.start();
0462         }
0463     } else {
0464         qCWarning(KNEWSTUFFQUICK) << "Attempted to call restoreSearch() without a correctly initialized engine. You will likely get unexpected behaviour.";
0465     }
0466 }
0467 
0468 void Engine::storeSearch()
0469 {
0470     d->storedRequest = d->currentRequest;
0471 }
0472 
0473 #include "moc_quickengine.cpp"