LCOV - code coverage report
Current view: top level - src/utilities - UpdateChecker.cpp (source / functions) Coverage Total Hit
Test: coverage.info.cleaned Lines: 94.0 % 83 78
Test Date: 2026-03-23 10:19:47 Functions: 85.7 % 7 6

            Line data    Source code
       1              : #include "UpdateChecker.h"
       2              : #include "FangLogging.h"
       3              : 
       4              : #include <QRegularExpression>
       5              : #include <QVersionNumber>
       6              : 
       7              : // Only include FangApp.h when instance() is needed (not in tests)
       8              : #ifndef UPDATECHECKER_NO_FANGAPP
       9              : #include "../FangApp.h"
      10              : #endif
      11              : 
      12              : const QUrl UpdateChecker::UPDATE_FEED_URL = QUrl("https://getfang.com/feed.xml");
      13              : const int UpdateChecker::CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
      14              : 
      15           22 : UpdateChecker::UpdateChecker(QObject *parent, ParserInterface* injectedParser, SettingsInterface* injectedSettings) :
      16              :     FangObject(parent),
      17           22 :     parser(injectedParser ? injectedParser : new NewsParser(this)),
      18           22 :     settingsInterface(injectedSettings),
      19           22 :     timer(this),
      20           22 :     _latestVersion(""),
      21           22 :     _updateAvailable(false)
      22              : {
      23           22 :     connect(parser, &ParserInterface::done, this, &UpdateChecker::onParserDone);
      24           22 :     connect(&timer, &QTimer::timeout, this, &UpdateChecker::checkNow);
      25              : 
      26           22 :     timer.setInterval(CHECK_INTERVAL_MS);
      27           22 : }
      28              : 
      29            0 : void UpdateChecker::start()
      30              : {
      31              :     // Perform immediate check
      32            0 :     checkNow();
      33              : 
      34              :     // Start periodic checking
      35            0 :     timer.start();
      36            0 : }
      37              : 
      38            7 : void UpdateChecker::checkNow()
      39              : {
      40           14 :     qCDebug(logUtility) << "UpdateChecker: Checking for updates...";
      41            7 :     parser->parse(UPDATE_FEED_URL);
      42            7 : }
      43              : 
      44            7 : void UpdateChecker::onParserDone()
      45              : {
      46            7 :     if (parser->getResult() != ParserInterface::OK) {
      47            3 :         qCWarning(logUtility) << "UpdateChecker: Failed to fetch update feed:"
      48            2 :                               << parser->getResult();
      49            4 :         return;
      50              :     }
      51              : 
      52            6 :     RawFeed* feed = parser->getFeed();
      53            6 :     if (!feed || feed->items.isEmpty()) {
      54            2 :         qCWarning(logUtility) << "UpdateChecker: No items in update feed";
      55            1 :         return;
      56              :     }
      57              : 
      58              :     // Get the latest release (first item in feed)
      59            5 :     RawNews* latestRelease = feed->items.first();
      60            5 :     QString version = extractVersion(latestRelease->title);
      61              : 
      62            5 :     if (version.isEmpty()) {
      63            2 :         qCWarning(logUtility) << "UpdateChecker: Could not extract version from title:"
      64            1 :                               << latestRelease->title;
      65            1 :         return;
      66              :     }
      67              : 
      68            4 :     _latestVersion = version;
      69            8 :     qCDebug(logUtility) << "UpdateChecker: Latest version:" << _latestVersion
      70            4 :                         << "Current version:" << APP_VERSION;
      71              : 
      72              :     // Compare versions
      73            4 :     if (isNewerVersion(APP_VERSION, _latestVersion)) {
      74            2 :         _updateAvailable = true;
      75              : 
      76              :         // Check if we've already shown the dialog for this version
      77            2 :         SettingsInterface* settings = settingsInterface;
      78              : #ifndef UPDATECHECKER_NO_FANGAPP
      79              :         if (!settings) {
      80              :             settings = FangApp::instance()->getSettings();
      81              :         }
      82              : #endif
      83            2 :         if (!settings) {
      84            0 :             qCWarning(logUtility) << "UpdateChecker: No settings available";
      85            1 :             return;
      86              :         }
      87            2 :         QString lastShown = settings->getLastSeenVersion();
      88            2 :         if (lastShown == _latestVersion) {
      89            2 :             qCDebug(logUtility) << "UpdateChecker: Update available but already shown for version:"
      90            1 :                                 << _latestVersion;
      91            1 :             return;
      92              :         }
      93              : 
      94            2 :         qCInfo(logUtility) << "UpdateChecker: Update available! New version:" << _latestVersion;
      95            1 :         emit updateAvailable(_latestVersion);
      96            2 :     } else {
      97            4 :         qCDebug(logUtility) << "UpdateChecker: No update available";
      98            2 :         _updateAvailable = false;
      99              :     }
     100            5 : }
     101              : 
     102           11 : QString UpdateChecker::extractVersion(const QString& title)
     103              : {
     104              :     // Expected format: "Fang X.Y.Z" or "Fang X.Y.Z-beta" or "Fang X.Y.Z-beta.N"
     105              :     // Match version pattern after "Fang "
     106           11 :     static QRegularExpression versionPattern(R"(Fang\s+(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.]+)?))");
     107              : 
     108           11 :     QRegularExpressionMatch match = versionPattern.match(title);
     109           11 :     if (match.hasMatch()) {
     110            8 :         return match.captured(1);
     111              :     }
     112              : 
     113            3 :     return QString();
     114           11 : }
     115              : 
     116           13 : bool UpdateChecker::isNewerVersion(const QString& currentVersion, const QString& newVersion)
     117              : {
     118              :     // Parse version strings of format: X.Y.Z or X.Y.Z-suffix
     119           26 :     auto parseVersion = [](const QString& version) -> QPair<QVersionNumber, QString> {
     120           26 :         QString numericPart = version;
     121           26 :         QString suffix;
     122              : 
     123           26 :         int dashIndex = version.indexOf('-');
     124           26 :         if (dashIndex != -1) {
     125           11 :             numericPart = version.left(dashIndex);
     126           11 :             suffix = version.mid(dashIndex + 1);
     127              :         }
     128              : 
     129           52 :         return qMakePair(QVersionNumber::fromString(numericPart), suffix);
     130           26 :     };
     131              : 
     132           13 :     auto [currentNum, currentSuffix] = parseVersion(currentVersion);
     133           13 :     auto [newNum, newSuffix] = parseVersion(newVersion);
     134              : 
     135              :     // Compare numeric part of version string.
     136           13 :     int comparison = QVersionNumber::compare(newNum, currentNum);
     137           13 :     if (comparison > 0) {
     138              :         // New version has higher number.
     139            5 :         return true;
     140            8 :     } else if (comparison < 0) {
     141              :         // New version has lower number.
     142            2 :         return false;
     143              :     }
     144              : 
     145              :     // No suffix > any suffix (beta, alpha, etc.)
     146            6 :     if (newSuffix.isEmpty() && !currentSuffix.isEmpty()) {
     147              :         // New is release, current is pre-release.
     148            1 :         return true;
     149            5 :     } else if (!newSuffix.isEmpty() && currentSuffix.isEmpty()) {
     150              :         // New is pre-release, current is release.
     151            1 :         return false;
     152              :     }
     153              : 
     154              :     // Both have suffixes or both don't, so just compare strings at this point.
     155            4 :     return newSuffix > currentSuffix;
     156           13 : }
        

Generated by: LCOV version 2.0-1