File indexing completed on 2025-09-14 05:22:01

0001 /*
0002     SPDX-FileCopyrightText: 2022 Fushan Wen <qydwhotmail@gmail.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "potdengine.h"
0008 
0009 #include <chrono>
0010 
0011 #include <QDBusConnection>
0012 #include <QFileInfo>
0013 #include <QThreadPool>
0014 
0015 #include <KPluginFactory>
0016 
0017 #include "cachedprovider.h"
0018 #include "debug.h"
0019 
0020 using namespace std::chrono_literals;
0021 
0022 namespace
0023 {
0024 constinit bool s_networkInformationAvailable = false;
0025 
0026 bool isUsingMeteredConnection()
0027 {
0028     if (!s_networkInformationAvailable) {
0029         return false;
0030     }
0031     const auto instance = QNetworkInformation::instance();
0032     if (instance->supports(QNetworkInformation::Feature::Metered)) {
0033         return instance->isMetered();
0034     } else if (instance->supports(QNetworkInformation::Feature::TransportMedium)) {
0035         const auto transport = instance->transportMedium();
0036         return transport == QNetworkInformation::TransportMedium::Cellular //
0037             || transport == QNetworkInformation::TransportMedium::Bluetooth;
0038     }
0039     return false;
0040 }
0041 
0042 bool isNetworkConnected()
0043 {
0044     const auto instance = QNetworkInformation::instance();
0045     if (instance->supports(QNetworkInformation::Feature::Reachability) && instance->reachability() != QNetworkInformation::Reachability::Online) {
0046         return false;
0047     }
0048     return true;
0049 }
0050 }
0051 
0052 PotdClient::PotdClient(const KPluginMetaData &metadata, const QVariantList &args, QObject *parent)
0053     : QObject(parent)
0054     , m_metadata(metadata)
0055     , m_identifier(metadata.value(QStringLiteral("X-KDE-PlasmaPoTDProvider-Identifier")))
0056     , m_args(args)
0057 {
0058     // updateSource() will be called in PotdClient::setUpdateOverMeteredConnection(bool)
0059     // or PotdBackend::setUpdateOverMeteredConnection(bool)
0060 }
0061 
0062 void PotdClient::updateSource(bool refresh)
0063 {
0064     if (m_loading) {
0065         return;
0066     }
0067 
0068     setLoading(true);
0069 
0070     // Check whether it is cached already...
0071     // Use cache even if it's outdated when using metered connection
0072     const bool ignoreAge = m_doesUpdateOverMeteredConnection == 0 && isUsingMeteredConnection();
0073     if ((!refresh || ignoreAge /* Allow force refresh only when no cached image is available */) && CachedProvider::isCached(m_identifier, m_args, ignoreAge)) {
0074         qCDebug(WALLPAPERPOTD) << "A local cache is available for" << m_identifier << "with arguments" << m_args;
0075 
0076         CachedProvider *provider = new CachedProvider(m_identifier, m_args, this);
0077         connect(provider, &PotdProvider::finished, this, &PotdClient::slotFinished);
0078         connect(provider, &PotdProvider::error, this, &PotdClient::slotError);
0079         return;
0080     }
0081 
0082     if (auto url = CachedProvider::identifierToPath(m_identifier, m_args); QFileInfo::exists(url)) {
0083         setLocalUrl(url);
0084     }
0085 
0086     const auto pluginResult = KPluginFactory::instantiatePlugin<PotdProvider>(m_metadata, this, m_args);
0087 
0088     if (pluginResult) {
0089         qCDebug(WALLPAPERPOTD) << "Downloading wallpaper from" << m_identifier << m_args;
0090         connect(pluginResult.plugin, &PotdProvider::finished, this, &PotdClient::slotFinished);
0091         connect(pluginResult.plugin, &PotdProvider::error, this, &PotdClient::slotError);
0092     } else {
0093         qCWarning(WALLPAPERPOTD) << "Error loading PoTD plugin:" << pluginResult.errorString;
0094     }
0095 }
0096 
0097 void PotdClient::setUpdateOverMeteredConnection(int value)
0098 {
0099     // Don't return if values are the same because there can be multiple
0100     // backends. Instead, let updateSource() decide whether to update
0101     // the wallpaper.
0102 
0103     m_doesUpdateOverMeteredConnection = value;
0104     updateSource();
0105 }
0106 
0107 void PotdClient::slotFinished(PotdProvider *provider, const QImage &image)
0108 {
0109     setInfoUrl(provider->infoUrl());
0110     setRemoteUrl(provider->remoteUrl());
0111     setTitle(provider->title());
0112     setAuthor(provider->author());
0113 
0114     // Store in cache if it's not the response of a CachedProvider
0115     if (qobject_cast<CachedProvider *>(provider) == nullptr) {
0116         PotdProviderData data;
0117         data.remoteUrl = provider->remoteUrl();
0118         data.infoUrl = provider->infoUrl();
0119         data.title = provider->title();
0120         data.author = provider->author();
0121         data.remoteUrl = provider->remoteUrl();
0122         data.image = image;
0123 
0124         SaveImageThread *thread = new SaveImageThread(m_identifier, m_args, data);
0125         connect(thread, &SaveImageThread::done, this, &PotdClient::slotCached);
0126         QThreadPool::globalInstance()->start(thread);
0127     } else {
0128         // Is cache provider
0129         setLocalUrl(provider->localPath());
0130         setLoading(false);
0131         Q_EMIT done(this, true);
0132     }
0133 
0134     provider->deleteLater();
0135 }
0136 
0137 void PotdClient::slotError(PotdProvider *provider)
0138 {
0139     qCWarning(WALLPAPERPOTD) << m_identifier << "with arguments" << m_args
0140                              << "failed to fetch the remote wallpaper. Please check your Internet connection or system date.";
0141     provider->deleteLater();
0142     setLoading(false);
0143     Q_EMIT done(this, false);
0144 }
0145 
0146 void PotdClient::slotCached(const QString &localPath)
0147 {
0148     setLocalUrl(localPath);
0149     setLoading(false);
0150     Q_EMIT done(this, true);
0151 }
0152 
0153 void PotdClient::setLoading(bool status)
0154 {
0155     if (status == m_loading) {
0156         return;
0157     }
0158 
0159     m_loading = status;
0160     Q_EMIT loadingChanged();
0161 }
0162 
0163 void PotdClient::setLocalUrl(const QString &urlString)
0164 {
0165     if (m_localPath == urlString) {
0166         return;
0167     }
0168 
0169     m_localPath = urlString;
0170     Q_EMIT localUrlChanged();
0171 }
0172 
0173 void PotdClient::setInfoUrl(const QUrl &url)
0174 {
0175     if (m_infoUrl == url) {
0176         return;
0177     }
0178 
0179     m_infoUrl = url;
0180     Q_EMIT infoUrlChanged();
0181 }
0182 
0183 void PotdClient::setRemoteUrl(const QUrl &url)
0184 {
0185     if (m_remoteUrl == url) {
0186         return;
0187     }
0188 
0189     m_remoteUrl = url;
0190     Q_EMIT remoteUrlChanged();
0191 }
0192 
0193 void PotdClient::setTitle(const QString &title)
0194 {
0195     if (m_title == title) {
0196         return;
0197     }
0198 
0199     m_title = title;
0200     Q_EMIT titleChanged();
0201 }
0202 
0203 void PotdClient::setAuthor(const QString &author)
0204 {
0205     if (m_author == author) {
0206         return;
0207     }
0208 
0209     m_author = author;
0210     Q_EMIT authorChanged();
0211 }
0212 
0213 PotdEngine::PotdEngine(QObject *parent)
0214     : QObject(parent)
0215 {
0216     qRegisterMetaType<PotdProviderData>();
0217 
0218     loadPluginMetaData();
0219 
0220     connect(&m_checkDatesTimer, &QTimer::timeout, this, &PotdEngine::forceUpdateSource);
0221 
0222     int interval = QDateTime::currentDateTime().msecsTo(QDate::currentDate().addDays(1).startOfDay()) + 60000;
0223     m_checkDatesTimer.setInterval(interval);
0224     m_checkDatesTimer.start();
0225     qCDebug(WALLPAPERPOTD) << "Time to next update (h):" << m_checkDatesTimer.interval() / 1000.0 / 60.0 / 60.0;
0226 
0227     // Sleep checker
0228     QDBusConnection::systemBus().connect(QStringLiteral("org.freedesktop.login1"),
0229                                          QStringLiteral("/org/freedesktop/login1"),
0230                                          QStringLiteral("org.freedesktop.login1.Manager"),
0231                                          QStringLiteral("PrepareForSleep"),
0232                                          this,
0233                                          SLOT(slotPrepareForSleep(bool)));
0234     loadNetworkInformation();
0235 }
0236 
0237 PotdClient *PotdEngine::registerClient(const QString &identifier, const QVariantList &args)
0238 {
0239     auto pr = m_clientMap.equal_range(identifier);
0240 
0241     auto createClient = [this, &identifier, &args]() -> PotdClient * {
0242         auto pluginIt = m_providersMap.find(identifier);
0243 
0244         if (pluginIt == m_providersMap.end()) {
0245             // Not a valid identifier
0246             return nullptr;
0247         }
0248 
0249         qCDebug(WALLPAPERPOTD) << identifier << "is registered with arguments" << args;
0250         auto client = new PotdClient(pluginIt->second, args, this);
0251         m_clientMap.emplace(identifier, ClientPair{client, 1});
0252 
0253         return client;
0254     };
0255 
0256     while (pr.first != pr.second) {
0257         // find exact match
0258         if (pr.first->second.client->m_args == args) {
0259             pr.first->second.instanceCount++;
0260             qCDebug(WALLPAPERPOTD) << identifier << "is registered with arguments" << args << "Total client(s):" << pr.first->second.instanceCount;
0261             return pr.first->second.client;
0262         }
0263 
0264         pr.first++;
0265     }
0266 
0267     return createClient();
0268 }
0269 
0270 void PotdEngine::unregisterClient(const QString &identifier, const QVariantList &args)
0271 {
0272     auto [beginIt, endIt] = m_clientMap.equal_range(identifier);
0273 
0274     while (beginIt != endIt) {
0275         // find exact match
0276         if (beginIt->second.client->m_args == args) {
0277             beginIt->second.instanceCount--;
0278             qCDebug(WALLPAPERPOTD) << identifier << "with arguments" << args << "is unregistered. Remaining client(s):" << beginIt->second.instanceCount;
0279             if (!beginIt->second.instanceCount) {
0280                 delete beginIt->second.client;
0281                 m_clientMap.erase(beginIt);
0282                 qCDebug(WALLPAPERPOTD) << identifier << "with arguments" << args << "is freed.";
0283                 break;
0284             }
0285         }
0286 
0287         beginIt++;
0288     }
0289 }
0290 
0291 void PotdEngine::updateSource(bool refresh)
0292 {
0293     if (!isNetworkConnected()) {
0294         qCDebug(WALLPAPERPOTD) << "Network is not connected, so the backend will not update wallpapers.";
0295         return;
0296     }
0297 
0298     m_lastUpdateSuccess = true;
0299 
0300     for (const auto &[_, clientPair] : std::as_const(m_clientMap)) {
0301         if (clientPair.client->m_loading) {
0302             continue;
0303         }
0304 
0305         connect(clientPair.client, &PotdClient::done, this, &PotdEngine::slotDone);
0306         m_updateCount++;
0307         qCDebug(WALLPAPERPOTD) << clientPair.client->m_metadata.value(QStringLiteral("X-KDE-PlasmaPoTDProvider-Identifier")) << "starts updating wallpaper.";
0308         clientPair.client->updateSource(refresh);
0309     }
0310 }
0311 
0312 void PotdEngine::forceUpdateSource()
0313 {
0314     updateSource(true);
0315 }
0316 
0317 void PotdEngine::slotDone(PotdClient *client, bool success)
0318 {
0319     disconnect(client, &PotdClient::done, this, &PotdEngine::slotDone);
0320 
0321     qCDebug(WALLPAPERPOTD) << client->m_identifier << "with arguments" << client->m_args << (success ? "finished" : "failed")
0322                            << "updating the wallpaper. Remaining clients:" << m_updateCount - 1;
0323 
0324     if (!success) {
0325         m_lastUpdateSuccess = false;
0326     }
0327 
0328     if (!--m_updateCount) {
0329         // Do not update until next day, and delay 1minute to make sure last modified condition is satisfied.
0330         if (m_lastUpdateSuccess) {
0331             m_checkDatesTimer.setInterval(QDateTime::currentDateTime().msecsTo(QDate::currentDate().startOfDay().addDays(1)) + 60000);
0332         } else {
0333             m_checkDatesTimer.setInterval(10min);
0334         }
0335         m_checkDatesTimer.start();
0336         qCDebug(WALLPAPERPOTD) << "Time to next update (h):" << m_checkDatesTimer.interval() / 1000.0 / 60.0 / 60.0;
0337     }
0338 }
0339 
0340 void PotdEngine::slotPrepareForSleep(bool sleep)
0341 {
0342     if (sleep) {
0343         return;
0344     }
0345 
0346     // Resume from sleep
0347     // Always force update to work around the current date not being updated
0348     forceUpdateSource();
0349 }
0350 
0351 void PotdEngine::slotReachabilityChanged(QNetworkInformation::Reachability newReachability)
0352 {
0353     if (newReachability == QNetworkInformation::Reachability::Online) {
0354         qCDebug(WALLPAPERPOTD) << "Network is connected.";
0355         updateSource(false);
0356     }
0357 }
0358 
0359 void PotdEngine::slotIsMeteredChanged(bool isMetered)
0360 {
0361     if (isMetered) {
0362         return;
0363     }
0364 
0365     updateSource(false);
0366 }
0367 
0368 void PotdEngine::loadNetworkInformation()
0369 {
0370     if (!QNetworkInformation::loadDefaultBackend()) {
0371         return;
0372     }
0373 
0374     s_networkInformationAvailable = true;
0375     const auto instance = QNetworkInformation::instance();
0376 
0377     if (instance->supports(QNetworkInformation::Feature::Metered)) {
0378         connect(instance, &QNetworkInformation::isMeteredChanged, this, &PotdEngine::slotIsMeteredChanged);
0379     }
0380     if (instance->supports(QNetworkInformation::Feature::Reachability)) {
0381         connect(instance, &QNetworkInformation::reachabilityChanged, this, &PotdEngine::slotReachabilityChanged);
0382     }
0383 }
0384 
0385 void PotdEngine::loadPluginMetaData()
0386 {
0387     const auto plugins = KPluginMetaData::findPlugins(QStringLiteral("potd"));
0388 
0389     m_providersMap.clear();
0390     m_providersMap.reserve(plugins.size());
0391 
0392     for (const KPluginMetaData &metadata : plugins) {
0393         const QString identifier = metadata.value(QStringLiteral("X-KDE-PlasmaPoTDProvider-Identifier"));
0394         if (!identifier.isEmpty()) {
0395             m_providersMap.emplace(identifier, metadata);
0396         }
0397     }
0398 }