From 18fd4975ff6cf48d3d1fc80f837125d2367628b0 Mon Sep 17 00:00:00 2001 From: ShikiSuen Date: Thu, 7 Apr 2022 00:11:07 +0800 Subject: [PATCH] AppDelegate // Make UpdateAPI into a standalone module. --- Source/Modules/AppDelegate.swift | 160 +------------------- Source/Modules/IMEModules/apiUpdate.swift | 176 ++++++++++++++++++++++ vChewing.xcodeproj/project.pbxproj | 4 + 3 files changed, 185 insertions(+), 155 deletions(-) create mode 100644 Source/Modules/IMEModules/apiUpdate.swift diff --git a/Source/Modules/AppDelegate.swift b/Source/Modules/AppDelegate.swift index 912c789b..2de9b799 100644 --- a/Source/Modules/AppDelegate.swift +++ b/Source/Modules/AppDelegate.swift @@ -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) -> 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 { diff --git a/Source/Modules/IMEModules/apiUpdate.swift b/Source/Modules/IMEModules/apiUpdate.swift new file mode 100644 index 00000000..127acb12 --- /dev/null +++ b/Source/Modules/IMEModules/apiUpdate.swift @@ -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) -> 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 + } +} diff --git a/vChewing.xcodeproj/project.pbxproj b/vChewing.xcodeproj/project.pbxproj index a059fde2..1181b2e5 100644 --- a/vChewing.xcodeproj/project.pbxproj +++ b/vChewing.xcodeproj/project.pbxproj @@ -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 = ""; }; 5BD05C6427B2BBEF004C4F1D /* WindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WindowController.swift; sourceTree = ""; }; 5BD05C6527B2BBEF004C4F1D /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 5BDC1CF927FDF1310052C2B9 /* apiUpdate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = apiUpdate.swift; sourceTree = ""; }; 5BDCBB4227B4F6C600D0CC59 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/MainMenu.strings"; sourceTree = ""; }; 5BDCBB4327B4F6C600D0CC59 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/frmAboutWindow.strings"; sourceTree = ""; }; 5BDCBB4527B4F6C600D0CC59 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/frmPrefWindow.strings"; sourceTree = ""; }; @@ -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; };