You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

382 lines
13 KiB

// Copyright © 2015 Abhishek Banthia
import Cocoa
import CoreLoggerKit
class VersionUpdateHandler: NSObject {
enum VersionUpdateHandlerPriority {
case defaultPri
case low
case medium
case high
}
static let kSecondsInDay: Double = 86400.0
static let kMacAppStoreRefreshDelay: Double = 5.0
static let kMacRequestTimeout: Double = 60.0
static let kVersionCheckLastVersionKey = "VersionCheckLastVersionKey"
static let kVersionIgnoreVersionKey = "VersionCheckIgnoreVersionKey"
static let kMacAppStoreIDKey = "VersionCheckAppStoreIDKey"
static let kVersionLastCheckedKey = "VersionLastCheckedKey"
static let kVersionLastRemindedKey = "VersionLastRemindedKey"
static let sharedInstance = VersionUpdateHandler()
private var appStoreCountry: String!
private var applicationVersion: String!
private var applicationBundleID: String = Bundle.main.bundleIdentifier ?? "N/A"
private var updatePriority = VersionUpdateHandlerPriority.defaultPri
private var useAllAvailableLanguages: Bool = true
private var onlyPromptIfMainWindowIsAvailable: Bool = true
private var checkAtLaunch: Bool = true
private var checkPeriod: Float = 0.0
private var remindPeriod: Float = 1.0
private var verboseLogging: Bool = true
private var updateURL: URL!
private var checkingForNewVersion: Bool = false
private var remoteVersionsDict: [String: Any] = [:]
private var downloadError: Error?
private var showOnFirstLaunch: Bool = false
public var previewMode: Bool = false
private var versionDetails: String?
override init() {
// Setup App Store Country
appStoreCountry = Locale.current.regionCode
if appStoreCountry == "150" {
appStoreCountry = "eu"
} else if appStoreCountry.replacingOccurrences(of: "[A-Za-z]{2}", with: "", options: .regularExpression, range: appStoreCountry.startIndex ..< appStoreCountry.endIndex).isEmpty == false {
appStoreCountry = "us"
}
// Setup App Version
var appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
if appVersion == nil {
appVersion = Bundle.main.object(forInfoDictionaryKey: kCFBundleVersionKey as String) as? String
}
applicationVersion = appVersion ?? "N/A"
// Bundle Identifier
super.init()
}
private func inThisVersionTitle() -> String {
return "New in this version"
}
private func updateAvailableTitle() -> String {
return "New version available"
}
private func versionLabelFormat() -> String {
return "Version %@"
}
private func okayButtonLabel() -> String {
return "OK"
}
private func ignoreButtonLabel() -> String {
return "Ignore"
}
private func downloadButtonLabel() -> String {
return "Download"
}
private func remindButtonLabel() -> String {
return "Remind Me Later"
}
private func updatedURL() -> URL {
if updateURL.absoluteString.isEmpty == false {
return updateURL
}
guard let appStoreId = appStoreID() else {
Logger.info("No App Store ID was found for Clocker")
return URL(string: "")!
}
return URL(string: "macappstore://itunes.apple.com/app/id\(appStoreId)")!
}
private func appStoreID() -> Int? {
return UserDefaults.standard.integer(forKey: VersionUpdateHandler.kMacAppStoreIDKey)
}
func setAppStoreID(_ appStoreID: Int) {
UserDefaults.standard.set(appStoreID, forKey: VersionUpdateHandler.kMacAppStoreIDKey)
}
private func setLastChecked(_ date: Date) {
UserDefaults.standard.set(date, forKey: VersionUpdateHandler.kVersionLastCheckedKey)
}
private func lastChecked() -> Date? {
return UserDefaults.standard.object(forKey: VersionUpdateHandler.kVersionLastCheckedKey) as? Date
}
private func setLastReminded(_ date: Date?) {
UserDefaults.standard.set(date, forKey: VersionUpdateHandler.kVersionLastRemindedKey)
}
private func lastReminded() -> Date? {
return UserDefaults.standard.object(forKey: VersionUpdateHandler.kVersionLastRemindedKey) as? Date
}
private func ignoredVersion() -> String? {
return UserDefaults.standard.object(forKey: VersionUpdateHandler.kVersionIgnoreVersionKey) as? String
}
private func setIgnoredVersion(_ version: String) {
UserDefaults.standard.set(version, forKey: VersionUpdateHandler.kVersionIgnoreVersionKey)
}
private func setViewedVersionDetails(_ viewed: Bool) {
UserDefaults.standard.set(viewed ? applicationVersion : nil, forKey: VersionUpdateHandler.kVersionCheckLastVersionKey)
}
private func viewedVersionDetails() -> Bool {
let lastVersionKey = UserDefaults.standard.object(forKey: VersionUpdateHandler.kVersionCheckLastVersionKey) as? String ?? ""
return lastVersionKey == applicationVersion
}
private func lastVersion() -> String {
return UserDefaults.standard.object(forKey: VersionUpdateHandler.kVersionCheckLastVersionKey) as? String ?? ""
}
private func setLastVersion(_ version: String) {
UserDefaults.standard.set(version, forKey: VersionUpdateHandler.kVersionCheckLastVersionKey)
}
private func localVersionsDict() -> [String: Any] {
return [String: Any]()
}
private func versionDetails(_ version: String, _ dict: [String: Any]) -> String? {
if let versionData = dict[version] as? String {
return versionData
} else if let versionDataArray = dict[version] as? NSArray {
return versionDataArray.componentsJoined(by: "\n")
}
return nil
}
private func versionDetails(since lastVersion: String, in dict: [String: Any]) -> String? {
var lastVersionCopy = lastVersion
if previewMode {
lastVersionCopy = "0"
}
var newVersionFound = false
var details: String = ""
let versions = dict.keys.sorted()
for version in versions {
if version.compareVersion(lastVersionCopy) == .orderedDescending {
newVersionFound = true
}
details.append(versionDetails(version, dict) ?? "")
details.append("\n")
}
if newVersionFound {
return details.trimmingCharacters(in: CharacterSet.newlines)
}
return nil
}
private func shouldCheckForNewVersion() -> Bool {
return true
}
private func applicationLaunched() {
if checkAtLaunch {
checkIfNewVersion()
} else if verboseLogging {
Logger.info("iVersion will not check for updatess because checkAtLaunch option is disabled")
}
}
private func versionDetailsString() -> String {
if versionDetails == nil {
if viewedVersionDetails() {
versionDetails = versionDetails(applicationVersion, localVersionsDict())
}
} else {
versionDetails = versionDetails(since: lastVersion(), in: localVersionsDict())
}
return versionDetails!
}
private func mostRecentVersionInDict(_ dict: [String: Any]) -> String {
// return [dictionary.allKeys sortedArrayUsingSelector:@selector(compareVersion:)].lastObject;
// TODO: Fix this sorting
return dict.keys.sorted().last ?? ""
}
private func showAlertWithTitle(_ title: String, _ details: String, _ defaultButton: String, _ ignoreButton: String, _ remindButton: String) -> NSAlert {
let floatMax = CGFloat.greatestFiniteMagnitude
let alert = NSAlert()
alert.messageText = title
alert.informativeText = inThisVersionTitle()
alert.addButton(withTitle: defaultButton)
let scrollView = NSScrollView(frame: NSRect(x: 0.0,
y: 0.0,
width: 380.0,
height: 15.0))
let contentSize = scrollView.contentSize
scrollView.borderType = .bezelBorder
scrollView.hasVerticalScroller = true
scrollView.hasHorizontalScroller = false
scrollView.autoresizingMask = [.width, .height]
let textView = NSTextView(frame: NSRect(x: 0.0,
y: 0.0,
width: contentSize.width,
height: contentSize.height))
textView.minSize = NSMakeSize(0.0, contentSize.height)
textView.maxSize = NSMakeSize(floatMax, floatMax)
textView.isVerticallyResizable = true
textView.isHorizontallyResizable = false
textView.isEditable = false
textView.autoresizingMask = .width
textView.textContainer?.containerSize = NSMakeSize(contentSize.width, floatMax)
textView.textContainer?.widthTracksTextView = true
textView.string = details
scrollView.documentView = textView
textView.sizeToFit()
let height = min(200.0, scrollView.documentView?.frame.size.height ?? 200.0) + 3.0
scrollView.frame = NSMakeRect(0.0, 0.0, scrollView.frame.size.width, height)
alert.accessoryView = scrollView
if ignoreButton.isEmpty == false {
alert.addButton(withTitle: ignoreButton)
}
if remindButton.isEmpty == false {
alert.addButton(withTitle: remindButton)
let modalResponse = alert.runModal()
if modalResponse == .alertFirstButtonReturn {
// Right most button
didDismissAlert(alert, 0)
} else if modalResponse == .alertSecondButtonReturn {
didDismissAlert(alert, 1)
} else {
didDismissAlert(alert, 2)
}
}
return alert
}
private func showIgnoreButton() -> Bool {
return false
}
private func showRemindButtton() -> Bool {
return false
}
private func didDismissAlert(_: NSAlert, _: Int) {
// Get Button Indice
}
private func downloadVersionsData() {
if onlyPromptIfMainWindowIsAvailable {
guard NSApplication.shared.mainWindow != nil else {
return
}
_ = Repeater(interval: .seconds(0.5), mode: .infinite) { _ in
OperationQueue.main.addOperation { [weak self] in
guard let self = self else {
return
}
self.downloadVersionsData()
}
}
}
if checkingForNewVersion {
checkingForNewVersion = false
if remoteVersionsDict.isEmpty {
if downloadError != nil {
Logger.info("Update Check Failed because of \(downloadError!.localizedDescription)")
} else {
Logger.info("Version Update Check because an unknown error occurred")
}
}
return
}
let details = versionDetails(since: applicationVersion, in: remoteVersionsDict)
let mostRecentVersion = mostRecentVersionInDict(remoteVersionsDict)
if details != nil {
// Check if ignored
let showDetails = ignoredVersion() == mostRecentVersion || previewMode
if showDetails {}
}
}
private func checkIfNewVersion() {
if onlyPromptIfMainWindowIsAvailable {
guard NSApplication.shared.mainWindow != nil else {
return
}
_ = Repeater(interval: .seconds(5), mode: .infinite) { _ in
OperationQueue.main.addOperation { [weak self] in
guard let self = self else {
return
}
self.checkIfNewVersion()
}
}
}
let lastVersionString = lastVersion()
if lastVersionString.isEmpty == false || showOnFirstLaunch || previewMode {
if applicationVersion.compareVersion(lastVersionString) == ComparisonResult.orderedDescending || previewMode {
// Clear Reminder
setLastReminded(nil)
}
}
}
}
extension String {
func compareVersion(_ version: String) -> ComparisonResult {
return compare(version,
options: CompareOptions.numeric,
range: nil,
locale: nil)
}
func compareVersionDescending(_ version: String) -> ComparisonResult {
let comparsionResult = (0 - compareVersion(version).rawValue)
switch comparsionResult {
case -1:
return ComparisonResult.orderedAscending
case 0:
return ComparisonResult.orderedSame
case 1:
return ComparisonResult.orderedDescending
default:
assertionFailure("Invalid Comparison Result")
return .orderedSame
}
}
}