From ea4ef1655bdadf04c36768f0f641ca7579f754cf Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Wed, 20 Apr 2022 00:34:17 -0400 Subject: [PATCH] Create `SparkleUpdater` class for access from Qt/C++ To actually get automatic updates going, all that needs to happen is that `SparkleUpdater` needs to be initialized. The rest of the functions can be connected to elements in the UI. --- launcher/Application.cpp | 1 + launcher/CMakeLists.txt | 9 + launcher/updater/macsparkle/SparkleUpdater.h | 124 +++++++++++ launcher/updater/macsparkle/SparkleUpdater.mm | 206 ++++++++++++++++++ 4 files changed, 340 insertions(+) create mode 100644 launcher/updater/macsparkle/SparkleUpdater.h create mode 100644 launcher/updater/macsparkle/SparkleUpdater.mm 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 + * + * 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 . + */ + +#ifndef LAUNCHER_SPARKLEUPDATER_H +#define LAUNCHER_SPARKLEUPDATER_H + +#include +#include + +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 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& 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 + * + * 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 . + */ + +#include "SparkleUpdater.h" + +#include "Application.h" + +#include +#include + +@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 *)change + context:(void *)context +{ + if ([keyPath isEqualToString:@"updater.canCheckForUpdates"]) + { + bool canCheck = [change[NSKeyValueChangeNewKey] boolValue]; + self.callback(canCheck); + } +} + +@end + + +@interface UpdaterDelegate : NSObject + +@property(nonatomic, copy) NSSet *allowedChannels; + +@end + +@implementation UpdaterDelegate + +- (NSSet *)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 SparkleUpdater::getAllowedChannels() +{ + // Convert NSSet -> QSet + __block QSet 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 *nsChannels = [NSSet setWithObject:channel.toNSString()]; + priv->updaterDelegate.allowedChannels = nsChannels; + qDebug() << channel; + APPLICATION->settings()->set("UpdateChannel", channel); +} + +void SparkleUpdater::setAllowedChannels(const QSet &channels) +{ + if (channels.isEmpty()) + { + clearAllowedChannels(); + return; + } + + QString channelsConfig = ""; + // Convert QSet -> NSSet + NSMutableSet *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::fromList(channelList); + setAllowedChannels(channels); +}