File indexing completed on 2024-05-12 16:59:42

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