aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--launcher/Application.cpp1
-rw-r--r--launcher/CMakeLists.txt9
-rw-r--r--launcher/updater/macsparkle/SparkleUpdater.h124
-rw-r--r--launcher/updater/macsparkle/SparkleUpdater.mm206
4 files changed, 340 insertions, 0 deletions
diff --git a/launcher/Application.cpp b/launcher/Application.cpp
index dc8a7b0d..456ea02c 100644
--- a/launcher/Application.cpp
+++ b/launcher/Application.cpp
@@ -544,6 +544,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
{
m_settings.reset(new INISettingsObject(BuildConfig.LAUNCHER_CONFIGFILE, this));
// Updates
+ // Multiple channels are separated by spaces
m_settings->registerSetting("UpdateChannel", BuildConfig.VERSION_CHANNEL);
m_settings->registerSetting("AutoUpdate", true);
diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt
index 87ce3b68..dc10c38e 100644
--- a/launcher/CMakeLists.txt
+++ b/launcher/CMakeLists.txt
@@ -164,6 +164,11 @@ set(UPDATE_SOURCES
updater/DownloadTask.cpp
)
+set(MAC_UPDATE_SOURCES
+ updater/macsparkle/SparkleUpdater.h
+ updater/macsparkle/SparkleUpdater.mm
+)
+
add_unit_test(UpdateChecker
SOURCES updater/UpdateChecker_test.cpp
LIBS Launcher_logic
@@ -600,6 +605,10 @@ set(LOGIC_SOURCES
${ATLAUNCHER_SOURCES}
)
+if(APPLE)
+ set (LOGIC_SOURCES ${LOGIC_SOURCES} ${MAC_UPDATE_SOURCES})
+endif()
+
SET(LAUNCHER_SOURCES
# Application base
Application.h
diff --git a/launcher/updater/macsparkle/SparkleUpdater.h b/launcher/updater/macsparkle/SparkleUpdater.h
new file mode 100644
index 00000000..9768d960
--- /dev/null
+++ b/launcher/updater/macsparkle/SparkleUpdater.h
@@ -0,0 +1,124 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * PolyMC - Minecraft Launcher
+ * Copyright (C) 2022 Kenneth Chew <kenneth.c0@protonmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#ifndef LAUNCHER_SPARKLEUPDATER_H
+#define LAUNCHER_SPARKLEUPDATER_H
+
+#include <QObject>
+#include <QSet>
+
+class SparkleUpdater : public QObject
+{
+ Q_OBJECT
+
+public:
+ /*!
+ * Start the Sparkle updater, which automatically checks for updates if necessary.
+ */
+ SparkleUpdater();
+ ~SparkleUpdater();
+
+ /*!
+ * Check for updates manually, showing the user a progress bar and an alert if no updates are found.
+ */
+ void checkForUpdates();
+
+ /*!
+ * Indicates whether or not to check for updates automatically.
+ */
+ bool getAutomaticallyChecksForUpdates();
+
+ /*!
+ * Indicates the current automatic update check interval in seconds.
+ */
+ double getUpdateCheckInterval();
+
+ /*!
+ * Indicates the set of Sparkle channels the updater is allowed to find new updates from.
+ */
+ QSet<QString> getAllowedChannels();
+
+ /*!
+ * Set whether or not to check for updates automatically.
+ *
+ * As per Sparkle documentation, "By default, Sparkle asks users on second launch for permission if they want
+ * automatic update checks enabled and sets this property based on their response. If SUEnableAutomaticChecks is
+ * set in the Info.plist, this permission request is not performed however.
+ *
+ * Setting this property will persist in the host bundle’s user defaults. Only set this property if you need
+ * dynamic behavior (e.g. user preferences).
+ *
+ * The update schedule cycle will be reset in a short delay after the property’s new value is set. This is to allow
+ * reverting this property without kicking off a schedule change immediately."
+ */
+ void setAutomaticallyChecksForUpdates(bool check);
+
+ /*!
+ * Set the current automatic update check interval in seconds.
+ *
+ * As per Sparkle documentation, "Setting this property will persist in the host bundle’s user defaults. For this
+ * reason, only set this property if you need dynamic behavior (eg user preferences). Otherwise prefer to set
+ * SUScheduledCheckInterval directly in your Info.plist.
+ *
+ * The update schedule cycle will be reset in a short delay after the property’s new value is set. This is to allow
+ * reverting this property without kicking off a schedule change immediately."
+ */
+ void setUpdateCheckInterval(double seconds);
+
+ /*!
+ * Clears all allowed Sparkle channels, returning to the default updater channel behavior.
+ */
+ void clearAllowedChannels();
+
+ /*!
+ * Set a single Sparkle channel the updater is allowed to find new updates from.
+ *
+ * Items in the default channel can always be found, regardless of this setting. If an empty string is passed,
+ * return to the default behavior.
+ */
+ void setAllowedChannel(const QString& channel);
+
+ /*!
+ * Set a set of Sparkle channels the updater is allowed to find new updates from.
+ *
+ * Items in the default channel can always be found, regardless of this setting. If an empty set is passed,
+ * return to the default behavior.
+ */
+ void setAllowedChannels(const QSet<QString>& channels);
+
+signals:
+ /*!
+ * Emits whenever the user's ability to check for updates changes.
+ *
+ * As per Sparkle documentation, "An update check can be made by the user when an update session isn’t in progress,
+ * or when an update or its progress is being shown to the user. A user cannot check for updates when data (such
+ * as the feed or an update) is still being downloaded automatically in the background.
+ *
+ * This property is suitable to use for menu item validation for seeing if checkForUpdates can be invoked."
+ */
+ void canCheckForUpdatesChanged(bool canCheck);
+
+private:
+ class Private;
+
+ Private* priv;
+
+ void loadChannelsFromSettings();
+};
+
+#endif //LAUNCHER_SPARKLEUPDATER_H
diff --git a/launcher/updater/macsparkle/SparkleUpdater.mm b/launcher/updater/macsparkle/SparkleUpdater.mm
new file mode 100644
index 00000000..0d4119a4
--- /dev/null
+++ b/launcher/updater/macsparkle/SparkleUpdater.mm
@@ -0,0 +1,206 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * PolyMC - Minecraft Launcher
+ * Copyright (C) 2022 Kenneth Chew <kenneth.c0@protonmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "SparkleUpdater.h"
+
+#include "Application.h"
+
+#include <Cocoa/Cocoa.h>
+#include <Sparkle/Sparkle.h>
+
+@interface UpdaterObserver : NSObject
+
+@property(nonatomic, readonly) SPUUpdater* updater;
+
+/// A callback to run when the state of `canCheckForUpdates` for the `updater` changes.
+@property(nonatomic, copy) void (^callback) (bool);
+
+- (id)initWithUpdater:(SPUUpdater*)updater;
+
+@end
+
+@implementation UpdaterObserver
+
+- (id)initWithUpdater:(SPUUpdater*)updater
+{
+ self = [super init];
+ _updater = updater;
+ [self addObserver:self forKeyPath:@"updater.canCheckForUpdates" options:NSKeyValueObservingOptionNew context:nil];
+
+ return self;
+}
+
+- (void)observeValueForKeyPath:(NSString *)keyPath
+ ofObject:(id)object
+ change:(NSDictionary<NSKeyValueChangeKey, id> *)change
+ context:(void *)context
+{
+ if ([keyPath isEqualToString:@"updater.canCheckForUpdates"])
+ {
+ bool canCheck = [change[NSKeyValueChangeNewKey] boolValue];
+ self.callback(canCheck);
+ }
+}
+
+@end
+
+
+@interface UpdaterDelegate : NSObject <SPUUpdaterDelegate>
+
+@property(nonatomic, copy) NSSet<NSString *> *allowedChannels;
+
+@end
+
+@implementation UpdaterDelegate
+
+- (NSSet<NSString *> *)allowedChannelsForUpdater:(SPUUpdater *)updater
+{
+ return _allowedChannels;
+}
+
+@end
+
+
+class SparkleUpdater::Private
+{
+public:
+ SPUStandardUpdaterController *updaterController;
+ UpdaterObserver *updaterObserver;
+ UpdaterDelegate *updaterDelegate;
+ NSAutoreleasePool *autoReleasePool;
+};
+
+SparkleUpdater::SparkleUpdater()
+{
+ priv = new SparkleUpdater::Private();
+
+ // Enable Cocoa's memory management.
+ NSApplicationLoad();
+ priv->autoReleasePool = [[NSAutoreleasePool alloc] init];
+
+ // Delegate is used for setting/getting allowed update channels.
+ priv->updaterDelegate = [[UpdaterDelegate alloc] init];
+
+ // Controller is the interface for actually doing the updates.
+ priv->updaterController = [[SPUStandardUpdaterController alloc] initWithStartingUpdater:true
+ updaterDelegate:priv->updaterDelegate
+ userDriverDelegate:nil];
+
+ priv->updaterObserver = [[UpdaterObserver alloc] initWithUpdater:priv->updaterController.updater];
+ // Use KVO to run a callback that emits a Qt signal when `canCheckForUpdates` changes, so the UI can respond accordingly.
+ priv->updaterObserver.callback = ^(bool canCheck) {
+ emit canCheckForUpdatesChanged(canCheck);
+ };
+
+ loadChannelsFromSettings();
+}
+
+SparkleUpdater::~SparkleUpdater()
+{
+ [priv->updaterObserver removeObserver:priv->updaterObserver forKeyPath:@"updater.canCheckForUpdates"];
+
+ [priv->updaterController release];
+ [priv->updaterObserver release];
+ [priv->updaterDelegate release];
+ [priv->autoReleasePool release];
+ delete priv;
+}
+
+void SparkleUpdater::checkForUpdates()
+{
+ [priv->updaterController checkForUpdates:nil];
+}
+
+bool SparkleUpdater::getAutomaticallyChecksForUpdates()
+{
+ return priv->updaterController.updater.automaticallyChecksForUpdates;
+}
+
+double SparkleUpdater::getUpdateCheckInterval()
+{
+ return priv->updaterController.updater.updateCheckInterval;
+}
+
+QSet<QString> SparkleUpdater::getAllowedChannels()
+{
+ // Convert NSSet<NSString> -> QSet<QString>
+ __block QSet<QString> channels;
+ [priv->updaterDelegate.allowedChannels enumerateObjectsUsingBlock:^(NSString *channel, BOOL *stop)
+ {
+ channels.insert(QString::fromNSString(channel));
+ }];
+ return channels;
+}
+
+void SparkleUpdater::setAutomaticallyChecksForUpdates(bool check)
+{
+ priv->updaterController.updater.automaticallyChecksForUpdates = check ? YES : NO; // make clang-tidy happy
+}
+
+void SparkleUpdater::setUpdateCheckInterval(double seconds)
+{
+ priv->updaterController.updater.updateCheckInterval = seconds;
+}
+
+void SparkleUpdater::clearAllowedChannels()
+{
+ priv->updaterDelegate.allowedChannels = [NSSet set];
+ APPLICATION->settings()->set("UpdateChannel", "");
+}
+
+void SparkleUpdater::setAllowedChannel(const QString &channel)
+{
+ if (channel.isEmpty())
+ {
+ clearAllowedChannels();
+ return;
+ }
+
+ NSSet<NSString *> *nsChannels = [NSSet setWithObject:channel.toNSString()];
+ priv->updaterDelegate.allowedChannels = nsChannels;
+ qDebug() << channel;
+ APPLICATION->settings()->set("UpdateChannel", channel);
+}
+
+void SparkleUpdater::setAllowedChannels(const QSet<QString> &channels)
+{
+ if (channels.isEmpty())
+ {
+ clearAllowedChannels();
+ return;
+ }
+
+ QString channelsConfig = "";
+ // Convert QSet<QString> -> NSSet<NSString>
+ NSMutableSet<NSString *> *nsChannels = [NSMutableSet setWithCapacity:channels.count()];
+ foreach (const QString channel, channels)
+ {
+ [nsChannels addObject:channel.toNSString()];
+ channelsConfig += channel + " ";
+ }
+
+ priv->updaterDelegate.allowedChannels = nsChannels;
+ APPLICATION->settings()->set("UpdateChannel", channelsConfig.trimmed());
+}
+
+void SparkleUpdater::loadChannelsFromSettings()
+{
+ QStringList channelList = APPLICATION->settings()->get("UpdateChannel").toString().split(" ");
+ auto channels = QSet<QString>::fromList(channelList);
+ setAllowedChannels(channels);
+}