AppDelegate // Make UpdateAPI into a standalone module.
This commit is contained in:
parent
1d40945706
commit
18fd4975ff
|
@ -27,156 +27,6 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|||
import Cocoa
|
||||
import InputMethodKit
|
||||
|
||||
private let kCheckUpdateAutomatically = "CheckUpdateAutomatically"
|
||||
private let kNextUpdateCheckDateKey = "NextUpdateCheckDate"
|
||||
private let kUpdateInfoEndpointKey = "UpdateInfoEndpoint"
|
||||
private let kUpdateInfoSiteKey = "UpdateInfoSite"
|
||||
private let kVersionDescription = "VersionDescription"
|
||||
private let kNextCheckInterval: TimeInterval = 86400.0
|
||||
private let kTimeoutInterval: TimeInterval = 60.0
|
||||
|
||||
struct VersionUpdateReport {
|
||||
var siteUrl: URL?
|
||||
var currentShortVersion: String = ""
|
||||
var currentVersion: String = ""
|
||||
var remoteShortVersion: String = ""
|
||||
var remoteVersion: String = ""
|
||||
var versionDescription: String = ""
|
||||
}
|
||||
|
||||
enum VersionUpdateApiResult {
|
||||
case shouldUpdate(report: VersionUpdateReport)
|
||||
case noNeedToUpdate
|
||||
case ignored
|
||||
}
|
||||
|
||||
enum VersionUpdateApiError: Error, LocalizedError {
|
||||
case connectionError(message: String)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .connectionError(let message):
|
||||
return String(
|
||||
format: NSLocalizedString(
|
||||
"There may be no internet connection or the server failed to respond.\n\nError message: %@",
|
||||
comment: ""), message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct VersionUpdateApi {
|
||||
static func check(
|
||||
forced: Bool, callback: @escaping (Result<VersionUpdateApiResult, Error>) -> Void
|
||||
) -> URLSessionTask? {
|
||||
guard let infoDict = Bundle.main.infoDictionary,
|
||||
let updateInfoURLString = infoDict[kUpdateInfoEndpointKey] as? String,
|
||||
let updateInfoURL = URL(string: updateInfoURLString)
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let request = URLRequest(
|
||||
url: updateInfoURL, cachePolicy: .reloadIgnoringLocalCacheData,
|
||||
timeoutInterval: kTimeoutInterval)
|
||||
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
if let error = error {
|
||||
DispatchQueue.main.async {
|
||||
forced
|
||||
? callback(
|
||||
.failure(
|
||||
VersionUpdateApiError.connectionError(
|
||||
message: error.localizedDescription)))
|
||||
: callback(.success(.ignored))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
guard
|
||||
let plist = try PropertyListSerialization.propertyList(
|
||||
from: data ?? Data(), options: [], format: nil) as? [AnyHashable: Any],
|
||||
let remoteVersion = plist[kCFBundleVersionKey] as? String,
|
||||
let infoDict = Bundle.main.infoDictionary
|
||||
else {
|
||||
DispatchQueue.main.async {
|
||||
forced
|
||||
? callback(.success(.noNeedToUpdate))
|
||||
: callback(.success(.ignored))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Validate info (e.g. bundle identifier)
|
||||
// TODO: Use HTML to display change log, need a new key like UpdateInfoChangeLogURL for this
|
||||
|
||||
let currentVersion = infoDict[kCFBundleVersionKey as String] as? String ?? ""
|
||||
let result = currentVersion.compare(
|
||||
remoteVersion, options: .numeric, range: nil, locale: nil)
|
||||
|
||||
if result != .orderedAscending {
|
||||
DispatchQueue.main.async {
|
||||
forced
|
||||
? callback(.success(.noNeedToUpdate))
|
||||
: callback(.success(.ignored))
|
||||
}
|
||||
IME.prtDebugIntel(
|
||||
"vChewingDebug: Update // Order is not Ascending, assuming that there's no new version available."
|
||||
)
|
||||
return
|
||||
}
|
||||
IME.prtDebugIntel(
|
||||
"vChewingDebug: Update // New version detected, proceeding to the next phase.")
|
||||
guard let siteInfoURLString = plist[kUpdateInfoSiteKey] as? String,
|
||||
let siteInfoURL = URL(string: siteInfoURLString)
|
||||
else {
|
||||
DispatchQueue.main.async {
|
||||
forced
|
||||
? callback(.success(.noNeedToUpdate))
|
||||
: callback(.success(.ignored))
|
||||
}
|
||||
IME.prtDebugIntel(
|
||||
"vChewingDebug: Update // Failed from retrieving / parsing URL intel.")
|
||||
return
|
||||
}
|
||||
IME.prtDebugIntel(
|
||||
"vChewingDebug: Update // URL intel retrieved, proceeding to the next phase.")
|
||||
var report = VersionUpdateReport(siteUrl: siteInfoURL)
|
||||
var versionDescription = ""
|
||||
let versionDescriptions = plist[kVersionDescription] as? [AnyHashable: Any]
|
||||
if let versionDescriptions = versionDescriptions {
|
||||
var locale = "en"
|
||||
let supportedLocales = ["en", "zh-Hant", "zh-Hans", "ja"]
|
||||
let preferredTags = Bundle.preferredLocalizations(from: supportedLocales)
|
||||
if let first = preferredTags.first {
|
||||
locale = first
|
||||
}
|
||||
versionDescription =
|
||||
versionDescriptions[locale] as? String ?? versionDescriptions["en"]
|
||||
as? String ?? ""
|
||||
if !versionDescription.isEmpty {
|
||||
versionDescription = "\n\n" + versionDescription
|
||||
}
|
||||
}
|
||||
report.currentShortVersion = infoDict["CFBundleShortVersionString"] as? String ?? ""
|
||||
report.currentVersion = currentVersion
|
||||
report.remoteShortVersion = plist["CFBundleShortVersionString"] as? String ?? ""
|
||||
report.remoteVersion = remoteVersion
|
||||
report.versionDescription = versionDescription
|
||||
DispatchQueue.main.async {
|
||||
callback(.success(.shouldUpdate(report: report)))
|
||||
}
|
||||
IME.prtDebugIntel("vChewingDebug: Update // Callbck Complete.")
|
||||
} catch {
|
||||
DispatchQueue.main.async {
|
||||
forced ? callback(.success(.noNeedToUpdate)) : callback(.success(.ignored))
|
||||
}
|
||||
}
|
||||
}
|
||||
task.resume()
|
||||
return task
|
||||
}
|
||||
}
|
||||
|
||||
@objc(AppDelegate)
|
||||
class AppDelegate: NSObject, NSApplicationDelegate, ctlNonModalAlertWindowDelegate,
|
||||
FSEventStreamHelperDelegate
|
||||
|
@ -220,7 +70,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, ctlNonModalAlertWindowDelega
|
|||
mgrPrefs.setMissingDefaults()
|
||||
|
||||
// 只要使用者沒有勾選檢查更新、沒有主動做出要檢查更新的操作,就不要檢查更新。
|
||||
if (UserDefaults.standard.object(forKey: kCheckUpdateAutomatically) != nil) == true {
|
||||
if (UserDefaults.standard.object(forKey: VersionUpdateApi.kCheckUpdateAutomatically) != nil) == true {
|
||||
checkForUpdate()
|
||||
}
|
||||
}
|
||||
|
@ -262,18 +112,18 @@ class AppDelegate: NSObject, NSApplicationDelegate, ctlNonModalAlertWindowDelega
|
|||
|
||||
// time for update?
|
||||
if !forced {
|
||||
if UserDefaults.standard.bool(forKey: kCheckUpdateAutomatically) == false {
|
||||
if UserDefaults.standard.bool(forKey: VersionUpdateApi.kCheckUpdateAutomatically) == false {
|
||||
return
|
||||
}
|
||||
let now = Date()
|
||||
let date = UserDefaults.standard.object(forKey: kNextUpdateCheckDateKey) as? Date ?? now
|
||||
let date = UserDefaults.standard.object(forKey: VersionUpdateApi.kNextUpdateCheckDateKey) as? Date ?? now
|
||||
if now.compare(date) == .orderedAscending {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
let nextUpdateDate = Date(timeInterval: kNextCheckInterval, since: Date())
|
||||
UserDefaults.standard.set(nextUpdateDate, forKey: kNextUpdateCheckDateKey)
|
||||
let nextUpdateDate = Date(timeInterval: VersionUpdateApi.kNextCheckInterval, since: Date())
|
||||
UserDefaults.standard.set(nextUpdateDate, forKey: VersionUpdateApi.kNextUpdateCheckDateKey)
|
||||
|
||||
checkTask = VersionUpdateApi.check(forced: forced) { [self] result in
|
||||
defer {
|
||||
|
|
|
@ -0,0 +1,176 @@
|
|||
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License).
|
||||
// All possible vChewing-specific modifications are of:
|
||||
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
|
||||
/*
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
1. The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
2. No trademark license is granted to use the trade names, trademarks, service
|
||||
marks, or product names of Contributor, except as required to fulfill notice
|
||||
requirements above.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import Cocoa
|
||||
|
||||
struct VersionUpdateReport {
|
||||
var siteUrl: URL?
|
||||
var currentShortVersion: String = ""
|
||||
var currentVersion: String = ""
|
||||
var remoteShortVersion: String = ""
|
||||
var remoteVersion: String = ""
|
||||
var versionDescription: String = ""
|
||||
}
|
||||
|
||||
enum VersionUpdateApiResult {
|
||||
case shouldUpdate(report: VersionUpdateReport)
|
||||
case noNeedToUpdate
|
||||
case ignored
|
||||
}
|
||||
|
||||
enum VersionUpdateApiError: Error, LocalizedError {
|
||||
case connectionError(message: String)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .connectionError(let message):
|
||||
return String(
|
||||
format: NSLocalizedString(
|
||||
"There may be no internet connection or the server failed to respond.\n\nError message: %@",
|
||||
comment: ""), message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct VersionUpdateApi {
|
||||
static let kCheckUpdateAutomatically = "CheckUpdateAutomatically"
|
||||
static let kNextUpdateCheckDateKey = "NextUpdateCheckDate"
|
||||
static let kUpdateInfoEndpointKey = "UpdateInfoEndpoint"
|
||||
static let kUpdateInfoSiteKey = "UpdateInfoSite"
|
||||
static let kVersionDescription = "VersionDescription"
|
||||
static let kNextCheckInterval: TimeInterval = 86400.0
|
||||
static let kTimeoutInterval: TimeInterval = 60.0
|
||||
static func check(
|
||||
forced: Bool, callback: @escaping (Result<VersionUpdateApiResult, Error>) -> Void
|
||||
) -> URLSessionTask? {
|
||||
guard let infoDict = Bundle.main.infoDictionary,
|
||||
let updateInfoURLString = infoDict[kUpdateInfoEndpointKey] as? String,
|
||||
let updateInfoURL = URL(string: updateInfoURLString)
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let request = URLRequest(
|
||||
url: updateInfoURL, cachePolicy: .reloadIgnoringLocalCacheData,
|
||||
timeoutInterval: kTimeoutInterval)
|
||||
let task = URLSession.shared.dataTask(with: request) { data, _, error in
|
||||
if let error = error {
|
||||
DispatchQueue.main.async {
|
||||
forced
|
||||
? callback(
|
||||
.failure(
|
||||
VersionUpdateApiError.connectionError(
|
||||
message: error.localizedDescription)))
|
||||
: callback(.success(.ignored))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
guard
|
||||
let plist = try PropertyListSerialization.propertyList(
|
||||
from: data ?? Data(), options: [], format: nil) as? [AnyHashable: Any],
|
||||
let remoteVersion = plist[kCFBundleVersionKey] as? String,
|
||||
let infoDict = Bundle.main.infoDictionary
|
||||
else {
|
||||
DispatchQueue.main.async {
|
||||
forced
|
||||
? callback(.success(.noNeedToUpdate))
|
||||
: callback(.success(.ignored))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Validate info (e.g. bundle identifier)
|
||||
// TODO: Use HTML to display change log, need a new key like UpdateInfoChangeLogURL for this
|
||||
|
||||
let currentVersion = infoDict[kCFBundleVersionKey as String] as? String ?? ""
|
||||
let result = currentVersion.compare(
|
||||
remoteVersion, options: .numeric, range: nil, locale: nil)
|
||||
|
||||
if result != .orderedAscending {
|
||||
DispatchQueue.main.async {
|
||||
forced
|
||||
? callback(.success(.noNeedToUpdate))
|
||||
: callback(.success(.ignored))
|
||||
}
|
||||
IME.prtDebugIntel(
|
||||
"vChewingDebug: Update // Order is not Ascending, assuming that there's no new version available."
|
||||
)
|
||||
return
|
||||
}
|
||||
IME.prtDebugIntel(
|
||||
"vChewingDebug: Update // New version detected, proceeding to the next phase.")
|
||||
guard let siteInfoURLString = plist[kUpdateInfoSiteKey] as? String,
|
||||
let siteInfoURL = URL(string: siteInfoURLString)
|
||||
else {
|
||||
DispatchQueue.main.async {
|
||||
forced
|
||||
? callback(.success(.noNeedToUpdate))
|
||||
: callback(.success(.ignored))
|
||||
}
|
||||
IME.prtDebugIntel(
|
||||
"vChewingDebug: Update // Failed from retrieving / parsing URL intel.")
|
||||
return
|
||||
}
|
||||
IME.prtDebugIntel(
|
||||
"vChewingDebug: Update // URL intel retrieved, proceeding to the next phase.")
|
||||
var report = VersionUpdateReport(siteUrl: siteInfoURL)
|
||||
var versionDescription = ""
|
||||
let versionDescriptions = plist[kVersionDescription] as? [AnyHashable: Any]
|
||||
if let versionDescriptions = versionDescriptions {
|
||||
var locale = "en"
|
||||
let supportedLocales = ["en", "zh-Hant", "zh-Hans", "ja"]
|
||||
let preferredTags = Bundle.preferredLocalizations(from: supportedLocales)
|
||||
if let first = preferredTags.first {
|
||||
locale = first
|
||||
}
|
||||
versionDescription =
|
||||
versionDescriptions[locale] as? String ?? versionDescriptions["en"]
|
||||
as? String ?? ""
|
||||
if !versionDescription.isEmpty {
|
||||
versionDescription = "\n\n" + versionDescription
|
||||
}
|
||||
}
|
||||
report.currentShortVersion = infoDict["CFBundleShortVersionString"] as? String ?? ""
|
||||
report.currentVersion = currentVersion
|
||||
report.remoteShortVersion = plist["CFBundleShortVersionString"] as? String ?? ""
|
||||
report.remoteVersion = remoteVersion
|
||||
report.versionDescription = versionDescription
|
||||
DispatchQueue.main.async {
|
||||
callback(.success(.shouldUpdate(report: report)))
|
||||
}
|
||||
IME.prtDebugIntel("vChewingDebug: Update // Callbck Complete.")
|
||||
} catch {
|
||||
DispatchQueue.main.async {
|
||||
forced ? callback(.success(.noNeedToUpdate)) : callback(.success(.ignored))
|
||||
}
|
||||
}
|
||||
}
|
||||
task.resume()
|
||||
return task
|
||||
}
|
||||
}
|
|
@ -50,6 +50,7 @@
|
|||
5BD05C6827B2BBEF004C4F1D /* Content.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BD05C6327B2BBEF004C4F1D /* Content.swift */; };
|
||||
5BD05C6927B2BBEF004C4F1D /* WindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BD05C6427B2BBEF004C4F1D /* WindowController.swift */; };
|
||||
5BD05C6A27B2BBEF004C4F1D /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BD05C6527B2BBEF004C4F1D /* ViewController.swift */; };
|
||||
5BDC1CFA27FDF1310052C2B9 /* apiUpdate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BDC1CF927FDF1310052C2B9 /* apiUpdate.swift */; };
|
||||
5BDCBB2E27B4E67A00D0CC59 /* vChewingPhraseEditor.app in Resources */ = {isa = PBXBuildFile; fileRef = 5BD05BB827B2A429004C4F1D /* vChewingPhraseEditor.app */; };
|
||||
5BE78BD927B3775B005EA1BE /* ctlAboutWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BE78BD827B37750005EA1BE /* ctlAboutWindow.swift */; };
|
||||
5BE78BDD27B3776D005EA1BE /* frmAboutWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5BE78BDA27B37764005EA1BE /* frmAboutWindow.xib */; };
|
||||
|
@ -212,6 +213,7 @@
|
|||
5BD05C6327B2BBEF004C4F1D /* Content.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Content.swift; sourceTree = "<group>"; };
|
||||
5BD05C6427B2BBEF004C4F1D /* WindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WindowController.swift; sourceTree = "<group>"; };
|
||||
5BD05C6527B2BBEF004C4F1D /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
|
||||
5BDC1CF927FDF1310052C2B9 /* apiUpdate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = apiUpdate.swift; sourceTree = "<group>"; };
|
||||
5BDCBB4227B4F6C600D0CC59 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/MainMenu.strings"; sourceTree = "<group>"; };
|
||||
5BDCBB4327B4F6C600D0CC59 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/frmAboutWindow.strings"; sourceTree = "<group>"; };
|
||||
5BDCBB4527B4F6C600D0CC59 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/frmPrefWindow.strings"; sourceTree = "<group>"; };
|
||||
|
@ -410,6 +412,7 @@
|
|||
5B62A32227AE756300A19448 /* IMEModules */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5BDC1CF927FDF1310052C2B9 /* apiUpdate.swift */,
|
||||
D4A13D5927A59D5C003BE359 /* ctlInputMethod.swift */,
|
||||
5BB802D927FABA8300CF1C19 /* ctlInputMethod_Menu.swift */,
|
||||
5B5E535127EF261400C6AA1E /* IME.swift */,
|
||||
|
@ -997,6 +1000,7 @@
|
|||
D41355DE278EA3ED005E5CBD /* UserPhrasesLM.mm in Sources */,
|
||||
6ACC3D3F27914F2400F1B140 /* KeyValueBlobReader.cpp in Sources */,
|
||||
D41355D8278D74B5005E5CBD /* mgrLangModel.mm in Sources */,
|
||||
5BDC1CFA27FDF1310052C2B9 /* apiUpdate.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue