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.
This commit is contained in:
Kenneth Chew 2022-04-20 00:34:17 -04:00
parent 7eb61a28be
commit ea4ef1655b
No known key found for this signature in database
GPG Key ID: F17D3E14A07739DA
4 changed files with 340 additions and 0 deletions

View File

@ -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);

View File

@ -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

View File

@ -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 bundles 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 propertys 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 bundles 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 propertys 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 isnt 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

View File

@ -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);
}