mirror of
https://github.com/keymanapp/keyman.git
synced 2026-08-19 06:47:41 +00:00
Split the downloading management into own class
This commit is contained in:
parent
50afdae060
commit
d05038edf2
3 changed files with 506 additions and 483 deletions
|
|
@ -215,6 +215,7 @@
|
|||
CE17ABDE23069E76005FBB14 /* LanguageResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE17ABDD23069E76005FBB14 /* LanguageResource.swift */; };
|
||||
CE1E1EC12303C8CC001C7BE0 /* ResourceDownloadStatusToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1E1EC02303C8CC001C7BE0 /* ResourceDownloadStatusToolbar.swift */; };
|
||||
CE1F67A32304EB3800FF6972 /* ResourceDownloadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1F67A22304EB3800FF6972 /* ResourceDownloadManager.swift */; };
|
||||
CE22DFBA230B94DB00A4551C /* ResourceDownloadQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE22DFB9230B94DB00A4551C /* ResourceDownloadQueue.swift */; };
|
||||
CE24ECF021B763740052D291 /* KeymanResponder+Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE24ECEF21B763740052D291 /* KeymanResponder+Types.swift */; };
|
||||
CE24ECF121B763740052D291 /* KeymanResponder+Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE24ECEF21B763740052D291 /* KeymanResponder+Types.swift */; };
|
||||
CE24ECF221B763740052D291 /* KeymanResponder+Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE24ECEF21B763740052D291 /* KeymanResponder+Types.swift */; };
|
||||
|
|
@ -420,6 +421,7 @@
|
|||
CE17ABDD23069E76005FBB14 /* LanguageResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageResource.swift; sourceTree = "<group>"; };
|
||||
CE1E1EC02303C8CC001C7BE0 /* ResourceDownloadStatusToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourceDownloadStatusToolbar.swift; sourceTree = "<group>"; };
|
||||
CE1F67A22304EB3800FF6972 /* ResourceDownloadManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourceDownloadManager.swift; sourceTree = "<group>"; };
|
||||
CE22DFB9230B94DB00A4551C /* ResourceDownloadQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourceDownloadQueue.swift; sourceTree = "<group>"; };
|
||||
CE24ECEF21B763740052D291 /* KeymanResponder+Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KeymanResponder+Types.swift"; sourceTree = "<group>"; };
|
||||
CE2B1E4521B60E7C007D092E /* DeviceKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = DeviceKit.framework; path = ../../Carthage/Build/iOS/DeviceKit.framework; sourceTree = "<group>"; };
|
||||
CE67D960228A6F190029F2B5 /* KeyboardCommandStructs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardCommandStructs.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -724,6 +726,7 @@
|
|||
C07A9D8D1FD1798900828ADD /* APIKeyboardRepository.swift */,
|
||||
9A9CB0812241704800231FB9 /* APILexicalModelRepository.swift */,
|
||||
CE1F67A22304EB3800FF6972 /* ResourceDownloadManager.swift */,
|
||||
CE22DFB9230B94DB00A4551C /* ResourceDownloadQueue.swift */,
|
||||
);
|
||||
path = KeyboardRepository;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -1286,6 +1289,7 @@
|
|||
9A079E40223B602B00581263 /* LexicalModel.swift in Sources */,
|
||||
C0E30C8C1FC40D0400C80416 /* Storage.swift in Sources */,
|
||||
C045148A1F85DF9100D88416 /* InputViewController.swift in Sources */,
|
||||
CE22DFBA230B94DB00A4551C /* ResourceDownloadQueue.swift in Sources */,
|
||||
9A9CB084224170F700231FB9 /* LexicalModelRepository.swift in Sources */,
|
||||
C06D37461F81F5C400F61AE0 /* PopoverView.swift in Sources */,
|
||||
9A4609992242047400B0BFD1 /* LexicalModelAPICall.swift in Sources */,
|
||||
|
|
|
|||
|
|
@ -7,252 +7,25 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import Reachability
|
||||
|
||||
private protocol DownloadNode { }
|
||||
|
||||
private class DownloadTask: DownloadNode {
|
||||
public enum Resource {
|
||||
case keyboard, lexicalModel, other
|
||||
}
|
||||
|
||||
public final var type: Resource
|
||||
public final var resources: [LanguageResource]?
|
||||
public final var request: HTTPDownloadRequest
|
||||
|
||||
public init(do request: HTTPDownloadRequest, for resources: [LanguageResource]?, type: DownloadTask.Resource) {
|
||||
self.request = request
|
||||
self.type = type
|
||||
self.resources = resources
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents one overall resource-related command for requests against the Keyman Cloud API.
|
||||
*/
|
||||
private class DownloadBatch: DownloadNode {
|
||||
public enum Activity {
|
||||
case download, update, composite
|
||||
}
|
||||
|
||||
/*
|
||||
* Three main cases so far:
|
||||
* - [.download, .keyboard]: download new Keyboard (possibly with an associated lexical model)
|
||||
* - [.download, .lexicalModel]: download new LexicalModel.
|
||||
* - [.composite, .other]: batch update of all resources, containing the following within its compositeQueue
|
||||
* - [.update, .keyboard]: updates one Keyboard
|
||||
* - [.update, .lexicalModel]: updates one LexicalModel
|
||||
* - Depending on needs, this may be extended to allow 'nested' .composite instances.
|
||||
*/
|
||||
public final var activity: Activity
|
||||
public final var type: DownloadTask.Resource
|
||||
|
||||
public final var tasks: [DownloadTask]?
|
||||
public final var batchQueue: [DownloadBatch]?
|
||||
//public final var promise: Int
|
||||
|
||||
public init?(do tasks: [DownloadTask], as activity: Activity, ofType type: DownloadTask.Resource) {
|
||||
self.activity = activity
|
||||
self.type = type
|
||||
self.tasks = tasks
|
||||
|
||||
// Indicates 'composite' mode, which should use the other initializer.
|
||||
if activity == .composite {
|
||||
return nil
|
||||
}
|
||||
|
||||
// A batch containing tasks should always be targeting a 'primary' language resource.
|
||||
// .other exists to handle fonts packaged with keyboards, not our primary resources.
|
||||
if type == .other {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.batchQueue = nil
|
||||
}
|
||||
|
||||
public init(queue: [DownloadBatch]) {
|
||||
self.activity = .composite
|
||||
self.type = .other
|
||||
self.batchQueue = queue
|
||||
|
||||
self.tasks = nil
|
||||
}
|
||||
}
|
||||
|
||||
private class DownloadQueueFrame {
|
||||
public var nodes: [DownloadNode] = []
|
||||
public var index: Int = 0
|
||||
private(set) var isComposite = false
|
||||
|
||||
public static func from(batch: DownloadBatch) {
|
||||
let frame = DownloadQueueFrame()
|
||||
|
||||
if batch.activity == .composite {
|
||||
frame.isComposite = true
|
||||
frame.nodes.append(contentsOf: batch.batchQueue!)
|
||||
} else {
|
||||
frame.nodes.append(contentsOf: batch.tasks!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class ResourceDownloadManager: HTTPDownloadDelegate {
|
||||
private var queueRoot: DownloadQueueFrame?
|
||||
private var batchQueue: [DownloadQueueFrame] = []
|
||||
|
||||
private var downloadQueue: HTTPDownloader? = nil
|
||||
var currentRequest: HTTPDownloadRequest?
|
||||
private var reachability: Reachability!
|
||||
private let keymanHostName = "api.keyman.com"
|
||||
|
||||
private var updateKbdQueue: [InstallableKeyboard]? = nil
|
||||
private var updateLexQueue: [InstallableLexicalModel]? = nil
|
||||
private var _isDoneButtonEnabled = false
|
||||
public class ResourceDownloadManager {
|
||||
private var downloader: ResourceDownloadQueue
|
||||
private var isDidUpdateCheck = false
|
||||
|
||||
public static let shared = ResourceDownloadManager()
|
||||
|
||||
private init() {
|
||||
//downloadQueue = HTTPDownloader(self) // TODO: Consider using a persistent one.
|
||||
reachability = Reachability(hostname: keymanHostName)
|
||||
}
|
||||
|
||||
deinit {
|
||||
// Just to be safe, we'll invalidate any pending download requests when this class is deinitialized.
|
||||
if let currentRequest = currentRequest {
|
||||
currentRequest.userInfo["completionBlock"] = nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Update checks + management
|
||||
|
||||
public func updatesAvailable() -> Bool {
|
||||
if Manager.shared.apiKeyboardRepository.languages == nil && Manager.shared.apiLexicalModelRepository.languages == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
isDidUpdateCheck = true
|
||||
let userKeyboards = Storage.active.userDefaults.userKeyboards
|
||||
let hasKbdUpdate = userKeyboards?.contains { keyboard in
|
||||
let kbID = keyboard.id
|
||||
return stateForKeyboard(withID: kbID) == .needsUpdate
|
||||
} ?? false
|
||||
|
||||
let userLexicalModels = Storage.active.userDefaults.userLexicalModels
|
||||
let hasLexUpdate = userLexicalModels?.contains { lexicalModel in
|
||||
let lmID = lexicalModel.id
|
||||
return stateForLexicalModel(withID: lmID) == .needsUpdate
|
||||
} ?? false
|
||||
|
||||
// FIXME: Testing only! Forces 'update'.
|
||||
return true
|
||||
//return hasKbdUpdate || hasLexUpdate
|
||||
}
|
||||
|
||||
// TODO: Not yet ready.
|
||||
public func performUpdates() {
|
||||
// The plan is to create new notifications to handle batch updates here, rather than
|
||||
// require a UI to manage the update queue.
|
||||
updateKeyboards()
|
||||
updateLexicalModels()
|
||||
}
|
||||
|
||||
private func updateKeyboards() {
|
||||
updateKbdQueue = []
|
||||
var kbIDs = Set<String>()
|
||||
|
||||
// Build the keyboard update queue
|
||||
Storage.active.userDefaults.userKeyboards?.forEach { kb in
|
||||
let kbState = stateForKeyboard(withID: kb.id)
|
||||
if kbState == .needsUpdate {
|
||||
if(!kbIDs.contains(kb.id)) {
|
||||
kbIDs.insert(kb.id)
|
||||
updateKbdQueue?.append(kb)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the keyboard update queue
|
||||
if !updateKbdQueue!.isEmpty {
|
||||
let langID = updateKbdQueue![0].languageID
|
||||
let kbID = updateKbdQueue![0].id
|
||||
downloadKeyboard(withID: kbID, languageID: langID, isUpdate: true)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateLexicalModels() {
|
||||
// Build the lexical model update queue
|
||||
updateLexQueue = []
|
||||
var lmIDs = Set<String>()
|
||||
|
||||
Storage.active.userDefaults.userLexicalModels?.forEach { lm in
|
||||
let lmState = stateForLexicalModel(withID: lm.id)
|
||||
if lmState == .needsUpdate {
|
||||
if !lmIDs.contains(lm.id) {
|
||||
lmIDs.insert(lm.id)
|
||||
updateLexQueue!.append(lm)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the lexical model update queue
|
||||
if !updateLexQueue!.isEmpty {
|
||||
let langID = updateLexQueue![0].languageID
|
||||
let lmID = updateLexQueue![0].id
|
||||
downloadLexicalModel(withID: lmID, languageID: langID, isUpdate: true)
|
||||
}
|
||||
downloader = ResourceDownloadQueue()
|
||||
}
|
||||
|
||||
// MARK: - Common functionality
|
||||
|
||||
private func checkCanExecute(_ batch: DownloadBatch) -> Bool {
|
||||
guard reachability.connection != Reachability.Connection.none else {
|
||||
let error = NSError(domain: "Keyman", code: 0,
|
||||
userInfo: [NSLocalizedDescriptionKey: "No internet connection"])
|
||||
downloadFailed(forBatch: batch, error: error)
|
||||
return false
|
||||
}
|
||||
|
||||
// At this stage, we now have everything needed to generate download requests.
|
||||
guard downloadQueue == nil else {
|
||||
let error = NSError(domain: "Keyman", code: 0,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Download queue is busy"])
|
||||
downloadFailed(forBatch: batch, error: error)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private func executeDownloadBatch(_ batch: DownloadBatch) {
|
||||
// TODO: Handling composite batches.
|
||||
downloadQueue = HTTPDownloader(self)
|
||||
downloadQueue!.userInfo = [Key.downloadBatch: batch]
|
||||
|
||||
batch.tasks?.forEach { task in
|
||||
downloadQueue!.addRequest(task.request)
|
||||
}
|
||||
|
||||
//currentBatch = batch
|
||||
queueRoot = DownloadQueueFrame()
|
||||
queueRoot!.nodes.append(batch)
|
||||
downloadQueue!.run()
|
||||
}
|
||||
|
||||
private func queueDownloadBatch(_ batch: DownloadBatch) {
|
||||
if queueRoot == nil {
|
||||
executeDownloadBatch(batch)
|
||||
} else {
|
||||
queueRoot?.nodes.append(batch)
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchHandler(for resourceType: DownloadTask.Resource, _ completionHandler: @escaping () -> Void)
|
||||
-> (_ error: Error?) -> Void {
|
||||
return { error in
|
||||
if let error = error {
|
||||
// TODO: Connect to an error handler (or just render appropriate text) based on the resource type.
|
||||
self.downloadFailed(forKeyboards: [], error: error)
|
||||
self.downloader.downloadFailed(forKeyboards: [], error: error)
|
||||
} else {
|
||||
log.info("Fetched repository. Continuing with download.")
|
||||
completionHandler()
|
||||
|
|
@ -269,7 +42,7 @@ public class ResourceDownloadManager: HTTPDownloadDelegate {
|
|||
let message = "Keyboard not found with id: \(keyboardID), languageID: \(languageID)"
|
||||
let error = NSError(domain: "Keyman", code: 0,
|
||||
userInfo: [NSLocalizedDescriptionKey: message])
|
||||
downloadFailed(forKeyboards: [], error: error)
|
||||
downloader.downloadFailed(forKeyboards: [], error: error)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -289,11 +62,11 @@ public class ResourceDownloadManager: HTTPDownloadDelegate {
|
|||
|
||||
// Perform common 'can download' check. We need positive reachability and no prior download queue.
|
||||
// The parameter facilitates error logging.
|
||||
if !checkCanExecute(dlBatch) {
|
||||
if !downloader.canExecute(dlBatch) {
|
||||
return nil
|
||||
}
|
||||
|
||||
queueDownloadBatch(dlBatch)
|
||||
downloader.queue(dlBatch)
|
||||
return dlBatch
|
||||
}
|
||||
return nil
|
||||
|
|
@ -359,7 +132,7 @@ public class ResourceDownloadManager: HTTPDownloadDelegate {
|
|||
} else {
|
||||
let message = "Keyboard repository not yet fetched"
|
||||
let error = NSError(domain: "Keyman", code: 0, userInfo: [NSLocalizedDescriptionKey: message])
|
||||
downloadFailed(forKeyboards: [], error: error)
|
||||
downloader.downloadFailed(forKeyboards: [], error: error)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -388,17 +161,17 @@ public class ResourceDownloadManager: HTTPDownloadDelegate {
|
|||
/// - Parameters:
|
||||
/// - url: URL to a JSON description of the keyboard
|
||||
public func downloadKeyboard(from url: URL) {
|
||||
guard reachability.connection != Reachability.Connection.none else {
|
||||
guard downloader.hasConnection() else {
|
||||
let error = NSError(domain: "Keyman", code: 0,
|
||||
userInfo: [NSLocalizedDescriptionKey: "No connection"])
|
||||
downloadFailed(forKeyboards: [], error: error)
|
||||
downloader.downloadFailed(forKeyboards: [], error: error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = try? Data(contentsOf: url) else {
|
||||
let error = NSError(domain: "Keyman", code: 0,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Failed to fetch JSON file"])
|
||||
downloadFailed(forKeyboards: [], error: error)
|
||||
downloader.downloadFailed(forKeyboards: [], error: error)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -422,7 +195,7 @@ public class ResourceDownloadManager: HTTPDownloadDelegate {
|
|||
let keyboard = try decoder.decode(KeyboardAPICall.self, from: data)
|
||||
downloadKeyboard(keyboard)
|
||||
} catch {
|
||||
downloadFailed(forKeyboards: [], error: error)
|
||||
downloader.downloadFailed(forKeyboards: [], error: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -447,7 +220,7 @@ public class ResourceDownloadManager: HTTPDownloadDelegate {
|
|||
public func stateForKeyboard(withID keyboardID: String) -> KeyboardState {
|
||||
// Needs validation - I don't think this if-condition can be met in Keyman's current state
|
||||
// (as of 2019-08-16)
|
||||
if keyboardIdForCurrentRequest() == keyboardID {
|
||||
if downloader.keyboardIdForCurrentRequest() == keyboardID {
|
||||
return .downloading
|
||||
}
|
||||
let userKeyboards = Storage.active.userDefaults.userKeyboards
|
||||
|
|
@ -466,22 +239,6 @@ public class ResourceDownloadManager: HTTPDownloadDelegate {
|
|||
return .upToDate
|
||||
}
|
||||
|
||||
// Needs validation - I don't think this function is practically utilized (as of 2019-08-16)
|
||||
func keyboardIdForCurrentRequest() -> String? {
|
||||
if let currentRequest = currentRequest {
|
||||
let tmpStr = currentRequest.url.lastPathComponent
|
||||
if tmpStr.hasJavaScriptExtension {
|
||||
return String(tmpStr.dropLast(3))
|
||||
}
|
||||
} else if let downloadQueue = downloadQueue {
|
||||
let kbInfo = downloadQueue.userInfo[Key.keyboardInfo]
|
||||
if let keyboards = kbInfo as? [InstallableKeyboard], let keyboard = keyboards.first {
|
||||
return keyboard.id
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MARK - Lexical models
|
||||
|
||||
private func getInstallableLexicalModelMetadata(withID lexicalModelID: String, languageID: String) -> InstallableLexicalModel? {
|
||||
|
|
@ -492,7 +249,7 @@ public class ResourceDownloadManager: HTTPDownloadDelegate {
|
|||
let error = NSError(domain: "Keyman", code: 0,
|
||||
userInfo: [NSLocalizedDescriptionKey: message])
|
||||
// TODO: better error target.
|
||||
downloadFailed(forLanguageID: "", error: error)
|
||||
downloader.downloadFailed(forLanguageID: "", error: error)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -512,11 +269,11 @@ public class ResourceDownloadManager: HTTPDownloadDelegate {
|
|||
|
||||
// Perform common 'can download' check. We need positive reachability and no prior download queue.
|
||||
// The parameter facilitates error logging.
|
||||
if !checkCanExecute(dlBatch) {
|
||||
if !downloader.canExecute(dlBatch) {
|
||||
return nil
|
||||
}
|
||||
|
||||
queueDownloadBatch(dlBatch)
|
||||
downloader.queue(dlBatch)
|
||||
return dlBatch
|
||||
}
|
||||
return nil
|
||||
|
|
@ -565,7 +322,7 @@ public class ResourceDownloadManager: HTTPDownloadDelegate {
|
|||
func listCompletionHandler(lexicalModels: [LexicalModel]?, error: Error?) -> Void {
|
||||
if let error = error {
|
||||
log.info("Failed to fetch lexical model list for "+languageID+". error: "+(error as! String))
|
||||
self.downloadFailed(forLanguageID: languageID, error: error) //???forKeyboards
|
||||
downloader.downloadFailed(forLanguageID: languageID, error: error)
|
||||
} else if nil == lexicalModels {
|
||||
//TODO: put up an alert instead
|
||||
log.info("No lexical models available for language \(languageID) (nil)")
|
||||
|
|
@ -616,7 +373,7 @@ public class ResourceDownloadManager: HTTPDownloadDelegate {
|
|||
} else {
|
||||
let message = "Keyboard repository not yet fetched"
|
||||
let error = NSError(domain: "Keyman", code: 0, userInfo: [NSLocalizedDescriptionKey: message])
|
||||
downloadFailed(forKeyboards: [], error: error)
|
||||
downloader.downloadFailed(forKeyboards: [], error: error)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -635,17 +392,17 @@ public class ResourceDownloadManager: HTTPDownloadDelegate {
|
|||
/// - Parameters:
|
||||
/// - url: URL to a JSON description of the lexical model
|
||||
public func downloadLexicalModel(from url: URL) {
|
||||
guard reachability.connection != Reachability.Connection.none else {
|
||||
guard downloader.hasConnection() else {
|
||||
let error = NSError(domain: "Keyman", code: 0,
|
||||
userInfo: [NSLocalizedDescriptionKey: "No connection"])
|
||||
downloadFailed(forKeyboards: [], error: error) //??? forLexicalModels
|
||||
downloader.downloadFailed(forKeyboards: [], error: error) //??? forLexicalModels
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = try? Data(contentsOf: url) else {
|
||||
let error = NSError(domain: "Keyman", code: 0,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Failed to fetch JSON file"])
|
||||
downloadFailed(forKeyboards: [], error: error) //??? forLexicalModels
|
||||
downloader.downloadFailed(forKeyboards: [], error: error) //??? forLexicalModels
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -669,7 +426,7 @@ public class ResourceDownloadManager: HTTPDownloadDelegate {
|
|||
let lexicalModel = try decoder.decode(LexicalModelAPICall.self, from: data)
|
||||
downloadLexicalModel(lexicalModel)
|
||||
} catch {
|
||||
downloadFailed(forKeyboards: [], error: error) //??? forLexicalModels
|
||||
downloader.downloadFailed(forKeyboards: [], error: error) //??? forLexicalModels
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -696,18 +453,18 @@ public class ResourceDownloadManager: HTTPDownloadDelegate {
|
|||
let isUpdate = Storage.active.userDefaults.userLexicalModels?.contains { $0.id == lexicalModel.id } ?? false
|
||||
|
||||
if let batch = buildLexicalModelDownloadBatch(for: installableLexicalModels[0], fromPath: lexicalModelURL, asActivity: isUpdate ? .update : .download) {
|
||||
if !checkCanExecute(batch) {
|
||||
if !downloader.canExecute(batch) {
|
||||
return
|
||||
}
|
||||
|
||||
queueDownloadBatch(batch)
|
||||
downloader.queue(batch)
|
||||
}
|
||||
}
|
||||
|
||||
/// - Returns: The current state for a lexical model
|
||||
//TODO: rename KeyboardState to ResourceState? so it can be used with both keybaoards and lexical models without confusion
|
||||
public func stateForLexicalModel(withID lexicalModelID: String) -> KeyboardState {
|
||||
if lexicalModelIdForCurrentRequest() == lexicalModelID {
|
||||
if downloader.lexicalModelIdForCurrentRequest() == lexicalModelID {
|
||||
return .downloading
|
||||
}
|
||||
let userLexicalModels = Storage.active.userDefaults.userLexicalModels
|
||||
|
|
@ -726,233 +483,84 @@ public class ResourceDownloadManager: HTTPDownloadDelegate {
|
|||
return .upToDate
|
||||
}
|
||||
|
||||
func lexicalModelIdForCurrentRequest() -> String? {
|
||||
if let currentRequest = currentRequest {
|
||||
let tmpStr = currentRequest.url.lastPathComponent
|
||||
if tmpStr.hasJavaScriptExtension {
|
||||
return String(tmpStr.dropLast(3))
|
||||
}
|
||||
} else if let downloadQueue = downloadQueue {
|
||||
let kbInfo = downloadQueue.userInfo[Key.lexicalModelInfo]
|
||||
if let lexicalModels = kbInfo as? [InstallableLexicalModel], let lexicalModel = lexicalModels.first {
|
||||
return lexicalModel.id
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// MARK: Update checks + management
|
||||
|
||||
// Processes fetched lexical models.
|
||||
// return a lexical model so caller can use it in a downloadSucceeded call
|
||||
// is called by other class funcs
|
||||
func installLexicalModelPackage(downloadedPackageFile: URL) -> InstallableLexicalModel? {
|
||||
var installedLexicalModel: InstallableLexicalModel? = nil
|
||||
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||
var destination = documentsDirectory
|
||||
destination.appendPathComponent("temp/\(downloadedPackageFile.lastPathComponent)")
|
||||
public func updatesAvailable() -> Bool {
|
||||
if Manager.shared.apiKeyboardRepository.languages == nil && Manager.shared.apiLexicalModelRepository.languages == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
isDidUpdateCheck = true
|
||||
let userKeyboards = Storage.active.userDefaults.userKeyboards
|
||||
let hasKbdUpdate = userKeyboards?.contains { keyboard in
|
||||
let kbID = keyboard.id
|
||||
return stateForKeyboard(withID: kbID) == .needsUpdate
|
||||
} ?? false
|
||||
|
||||
KeymanPackage.extract(fileUrl: downloadedPackageFile, destination: destination, complete: { kmp in
|
||||
if let kmp = kmp as! LexicalModelKeymanPackage? {
|
||||
do {
|
||||
try Manager.shared.parseLMKMP(kmp.sourceFolder)
|
||||
log.info("successfully parsed the lexical model in: \(kmp.sourceFolder)")
|
||||
installedLexicalModel = kmp.models[0].installableLexicalModels[0]
|
||||
//this can fail gracefully and not show errors to users
|
||||
try FileManager.default.removeItem(at: downloadedPackageFile)
|
||||
} catch {
|
||||
log.error("Error installing the lexical model: \(error)")
|
||||
}
|
||||
} else {
|
||||
log.error("Error extracting the lexical model from the package: \(KMPError.invalidPackage)")
|
||||
}
|
||||
})
|
||||
return installedLexicalModel
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MARK - deprecated helper/handler methods - they help avoid errors for now.
|
||||
|
||||
private func downloadFailed(forBatch batch: DownloadBatch, error: Error) {
|
||||
if batch.activity == .composite {
|
||||
// It's an update operation.
|
||||
// TODO: Needs implementation.
|
||||
} else if batch.type == .keyboard {
|
||||
let keyboards = batch.tasks?.compactMap { task in
|
||||
return task.resources as? [InstallableKeyboard]
|
||||
}.flatMap {$0}
|
||||
downloadFailed(forKeyboards: keyboards ?? [], error: error)
|
||||
} else if batch.type == .lexicalModel {
|
||||
let lexModels = batch.tasks?.compactMap { task in
|
||||
return task.resources as? [InstallableLexicalModel]
|
||||
}.flatMap {$0}
|
||||
downloadFailed(forLanguageID: lexModels![0].languageID, error: error)
|
||||
}
|
||||
}
|
||||
|
||||
private func downloadFailed(forKeyboards keyboards: [InstallableKeyboard], error: Error) {
|
||||
let notification = KeyboardDownloadFailedNotification(keyboards: keyboards, error: error)
|
||||
NotificationCenter.default.post(name: Notifications.keyboardDownloadFailed,
|
||||
object: self,
|
||||
value: notification)
|
||||
}
|
||||
|
||||
private func downloadFailed(forLanguageID languageID: String, error: Error) {
|
||||
let notification = LexicalModelDownloadFailedNotification(lmOrLanguageID: languageID, error: error)
|
||||
NotificationCenter.default.post(name: Notifications.lexicalModelDownloadFailed,
|
||||
object: self,
|
||||
value: notification)
|
||||
}
|
||||
|
||||
private func downloadFailed(forLexicalModelPackage packageURL: String, error: Error) {
|
||||
let notification = LexicalModelDownloadFailedNotification(lmOrLanguageID: packageURL, error: error)
|
||||
NotificationCenter.default.post(name: Notifications.lexicalModelDownloadFailed,
|
||||
object: self,
|
||||
value: notification)
|
||||
}
|
||||
|
||||
private func downloadSucceeded(forLexicalModel lm: InstallableLexicalModel) {
|
||||
let notification = LexicalModelDownloadCompletedNotification([lm])
|
||||
NotificationCenter.default.post(name: Notifications.lexicalModelDownloadCompleted,
|
||||
object: self,
|
||||
value: notification)
|
||||
}
|
||||
|
||||
//MARK: - HTTPDownloadDelegate methods
|
||||
|
||||
func downloadQueueFinished(_ queue: HTTPDownloader) {
|
||||
// We can use the properties of the current "batch" to generate specialized notifications.
|
||||
queueRoot = nil
|
||||
}
|
||||
|
||||
func downloadRequestStarted(_ request: HTTPDownloadRequest) {
|
||||
// If we're downloading a new keyboard.
|
||||
// The extra check is there to filter out other potential request types in the future.
|
||||
if request.tag == 0 {
|
||||
let task = request.userInfo[Key.downloadTask] as! DownloadTask
|
||||
if task.type == .keyboard {
|
||||
NotificationCenter.default.post(name: Notifications.keyboardDownloadStarted,
|
||||
object: self,
|
||||
value: task.resources as! [InstallableKeyboard])
|
||||
} else if task.type == .lexicalModel {
|
||||
NotificationCenter.default.post(name: Notifications.lexicalModelDownloadStarted,
|
||||
object: self,
|
||||
value: task.resources as! [InstallableLexicalModel])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func downloadRequestFinished(_ request: HTTPDownloadRequest) {
|
||||
let batch = request.userInfo[Key.downloadBatch] as! DownloadBatch
|
||||
let isUpdate = batch.activity == .update
|
||||
let userLexicalModels = Storage.active.userDefaults.userLexicalModels
|
||||
let hasLexUpdate = userLexicalModels?.contains { lexicalModel in
|
||||
let lmID = lexicalModel.id
|
||||
return stateForLexicalModel(withID: lmID) == .needsUpdate
|
||||
} ?? false
|
||||
|
||||
let task = request.userInfo[Key.downloadTask] as! DownloadTask
|
||||
// FIXME: Testing only! Forces 'update'.
|
||||
return true
|
||||
//return hasKbdUpdate || hasLexUpdate
|
||||
}
|
||||
|
||||
// TODO: Not yet ready.
|
||||
public func performUpdates() {
|
||||
// The plan is to create new notifications to handle batch updates here, rather than
|
||||
// require a UI to manage the update queue.
|
||||
updateKeyboards()
|
||||
updateLexicalModels()
|
||||
}
|
||||
|
||||
private func updateKeyboards() {
|
||||
var updateKbdQueue: [InstallableKeyboard]? = []
|
||||
var kbIDs = Set<String>()
|
||||
|
||||
// FIXME
|
||||
if let statusCode = request.responseStatusCode, statusCode == 200 {
|
||||
if task.type == .keyboard {
|
||||
// The request has succeeded.
|
||||
if downloadQueue!.requestsCount == 0 {
|
||||
let keyboards = task.resources as? [InstallableKeyboard]
|
||||
|
||||
// Download queue finished.
|
||||
downloadQueue = nil
|
||||
FontManager.shared.registerCustomFonts()
|
||||
log.info("Downloaded keyboard: \(keyboards![0].id).")
|
||||
|
||||
NotificationCenter.default.post(name: Notifications.keyboardDownloadCompleted,
|
||||
object: self,
|
||||
value: keyboards!)
|
||||
// TODO: Trigger by notification. Needs to be done on Manager.swift, not this class.
|
||||
// if isUpdate {
|
||||
// shouldReloadKeyboard = true
|
||||
// inputViewController.reload()
|
||||
// }
|
||||
let userDefaults = Storage.active.userDefaults
|
||||
userDefaults.set([Date()], forKey: Key.synchronizeSWKeyboard)
|
||||
userDefaults.synchronize()
|
||||
}
|
||||
} else if task.type == .lexicalModel {
|
||||
if let lm = installLexicalModelPackage(downloadedPackageFile: URL.init(string: task.request.destinationFile!)!) {
|
||||
downloadSucceeded(forLexicalModel: lm)
|
||||
} else {
|
||||
let installError = NSError(domain: "Keyman", code: 0,
|
||||
userInfo: [NSLocalizedDescriptionKey: "installError"])
|
||||
downloadFailed(forLexicalModelPackage: "\(task.request.url)", error: installError )
|
||||
}
|
||||
|
||||
// Temp - gotta clear the queue to match original behavior for now.
|
||||
if downloadQueue!.requestsCount == 0 {
|
||||
downloadQueue = nil
|
||||
// Build the keyboard update queue
|
||||
Storage.active.userDefaults.userKeyboards?.forEach { kb in
|
||||
let kbState = stateForKeyboard(withID: kb.id)
|
||||
if kbState == .needsUpdate {
|
||||
if(!kbIDs.contains(kb.id)) {
|
||||
kbIDs.insert(kb.id)
|
||||
updateKbdQueue?.append(kb)
|
||||
}
|
||||
}
|
||||
} else { // Possible request error (400 Bad Request, 404 Not Found, etc.)
|
||||
downloadQueue!.cancelAllOperations()
|
||||
downloadQueue = nil
|
||||
}
|
||||
|
||||
let errorMessage = "\(request.responseStatusMessage ?? ""): \(request.url)"
|
||||
let error = NSError(domain: "Keyman", code: 0,
|
||||
userInfo: [NSLocalizedDescriptionKey: errorMessage])
|
||||
|
||||
if task.type == .keyboard {
|
||||
let keyboards = task.resources as? [InstallableKeyboard]
|
||||
log.error("Keyboard download failed: \(error).")
|
||||
// Execute the keyboard update queue
|
||||
if !updateKbdQueue!.isEmpty {
|
||||
let langID = updateKbdQueue![0].languageID
|
||||
let kbID = updateKbdQueue![0].id
|
||||
downloadKeyboard(withID: kbID, languageID: langID, isUpdate: true)
|
||||
}
|
||||
}
|
||||
|
||||
if !isUpdate {
|
||||
// Clean up keyboard file if anything fails
|
||||
// TODO: Also clean up remaining fonts
|
||||
try? FileManager.default.removeItem(at: Storage.active.keyboardURL(for: keyboards![0]))
|
||||
private func updateLexicalModels() {
|
||||
// Build the lexical model update queue
|
||||
var updateLexQueue: [InstallableLexicalModel]? = []
|
||||
var lmIDs = Set<String>()
|
||||
|
||||
Storage.active.userDefaults.userLexicalModels?.forEach { lm in
|
||||
let lmState = stateForLexicalModel(withID: lm.id)
|
||||
if lmState == .needsUpdate {
|
||||
if !lmIDs.contains(lm.id) {
|
||||
lmIDs.insert(lm.id)
|
||||
updateLexQueue!.append(lm)
|
||||
}
|
||||
|
||||
downloadFailed(forKeyboards: keyboards ?? [], error: error)
|
||||
} else if task.type == .lexicalModel {
|
||||
let lexicalModels = task.resources as? [InstallableLexicalModel]
|
||||
log.error("Dictionary download failed: \(error).")
|
||||
|
||||
if !isUpdate {
|
||||
// Clean up keyboard file if anything fails
|
||||
// TODO: Also clean up remaining fonts
|
||||
try? FileManager.default.removeItem(at: Storage.active.lexicalModelURL(for: lexicalModels![0]))
|
||||
}
|
||||
|
||||
downloadFailed(forLanguageID: lexicalModels?[0].languageID ?? "", error: error)
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the lexical model update queue
|
||||
if !updateLexQueue!.isEmpty {
|
||||
let langID = updateLexQueue![0].languageID
|
||||
let lmID = updateLexQueue![0].id
|
||||
downloadLexicalModel(withID: lmID, languageID: langID, isUpdate: true)
|
||||
}
|
||||
}
|
||||
|
||||
//func downloadRequestSuccess(
|
||||
|
||||
func downloadRequestFailed(_ request: HTTPDownloadRequest) {
|
||||
switch request.typeCode {
|
||||
case .downloadFile:
|
||||
downloadQueue = nil
|
||||
let error = request.error!
|
||||
|
||||
let task = request.userInfo[Key.downloadTask] as! DownloadTask
|
||||
let batch = request.userInfo[Key.downloadBatch] as! DownloadBatch
|
||||
let isUpdate = batch.activity == .update
|
||||
|
||||
if task.type == .keyboard {
|
||||
log.error("Keyboard download failed: \(error).")
|
||||
let keyboards = task.resources as? [InstallableKeyboard]
|
||||
|
||||
if !isUpdate {
|
||||
// Clean up keyboard file if anything fails
|
||||
// TODO: Also clean up remaining fonts
|
||||
try? FileManager.default.removeItem(at: Storage.active.keyboardURL(for: keyboards![0]))
|
||||
}
|
||||
|
||||
downloadFailed(forKeyboards: keyboards ?? [], error: error as NSError)
|
||||
} else if task.type == .lexicalModel {
|
||||
log.error("Dictionary download failed: \(error).")
|
||||
let lexicalModels = task.resources as? [InstallableLexicalModel]
|
||||
|
||||
if !isUpdate {
|
||||
try? FileManager.default.removeItem(at: Storage.active.lexicalModelURL(for: lexicalModels![0]))
|
||||
}
|
||||
|
||||
downloadFailed(forLanguageID: lexicalModels?[0].languageID ?? "", error: error as NSError)
|
||||
}
|
||||
}
|
||||
}
|
||||
// End REWORK THIS SECTION ------
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,411 @@
|
|||
//
|
||||
// ResourceDownloadQueue.swift
|
||||
// KeymanEngine
|
||||
//
|
||||
// Created by Joshua Horton on 8/20/19.
|
||||
// Copyright © 2019 SIL International. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Reachability
|
||||
|
||||
private protocol DownloadNode { }
|
||||
|
||||
class DownloadTask: DownloadNode {
|
||||
public enum Resource {
|
||||
case keyboard, lexicalModel, other
|
||||
}
|
||||
|
||||
public final var type: Resource
|
||||
public final var resources: [LanguageResource]?
|
||||
public final var request: HTTPDownloadRequest
|
||||
|
||||
public init(do request: HTTPDownloadRequest, for resources: [LanguageResource]?, type: DownloadTask.Resource) {
|
||||
self.request = request
|
||||
self.type = type
|
||||
self.resources = resources
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents one overall resource-related command for requests against the Keyman Cloud API.
|
||||
*/
|
||||
class DownloadBatch: DownloadNode {
|
||||
public enum Activity {
|
||||
case download, update, composite
|
||||
}
|
||||
|
||||
/*
|
||||
* Three main cases so far:
|
||||
* - [.download, .keyboard]: download new Keyboard (possibly with an associated lexical model)
|
||||
* - [.download, .lexicalModel]: download new LexicalModel.
|
||||
* - [.composite, .other]: batch update of all resources, containing the following within its compositeQueue
|
||||
* - [.update, .keyboard]: updates one Keyboard
|
||||
* - [.update, .lexicalModel]: updates one LexicalModel
|
||||
* - Depending on needs, this may be extended to allow 'nested' .composite instances.
|
||||
*/
|
||||
public final var activity: Activity
|
||||
public final var type: DownloadTask.Resource
|
||||
|
||||
public final var tasks: [DownloadTask]?
|
||||
public final var batchQueue: [DownloadBatch]?
|
||||
//public final var promise: Int
|
||||
|
||||
public init?(do tasks: [DownloadTask], as activity: Activity, ofType type: DownloadTask.Resource) {
|
||||
self.activity = activity
|
||||
self.type = type
|
||||
self.tasks = tasks
|
||||
|
||||
// Indicates 'composite' mode, which should use the other initializer.
|
||||
if activity == .composite {
|
||||
return nil
|
||||
}
|
||||
|
||||
// A batch containing tasks should always be targeting a 'primary' language resource.
|
||||
// .other exists to handle fonts packaged with keyboards, not our primary resources.
|
||||
if type == .other {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.batchQueue = nil
|
||||
}
|
||||
|
||||
public init(queue: [DownloadBatch]) {
|
||||
self.activity = .composite
|
||||
self.type = .other
|
||||
self.batchQueue = queue
|
||||
|
||||
self.tasks = nil
|
||||
}
|
||||
}
|
||||
|
||||
private class DownloadQueueFrame {
|
||||
public var nodes: [DownloadNode] = []
|
||||
public var index: Int = 0
|
||||
private(set) var isComposite = false
|
||||
|
||||
public static func from(batch: DownloadBatch) {
|
||||
let frame = DownloadQueueFrame()
|
||||
|
||||
if batch.activity == .composite {
|
||||
frame.isComposite = true
|
||||
frame.nodes.append(contentsOf: batch.batchQueue!)
|
||||
} else {
|
||||
frame.nodes.append(contentsOf: batch.tasks!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ResourceDownloadQueue: HTTPDownloadDelegate {
|
||||
private var queueRoot: DownloadQueueFrame?
|
||||
private var batchQueue: [DownloadQueueFrame] = []
|
||||
|
||||
private var downloader: HTTPDownloader? = nil
|
||||
private var reachability: Reachability!
|
||||
private let keymanHostName = "api.keyman.com"
|
||||
|
||||
public init() {
|
||||
reachability = Reachability(hostname: keymanHostName)
|
||||
}
|
||||
|
||||
public func hasConnection() -> Bool {
|
||||
return reachability.connection != Reachability.Connection.none
|
||||
}
|
||||
|
||||
// Might should add a "withNotification: Bool" option for clarity.
|
||||
public func canExecute(_ batch: DownloadBatch) -> Bool {
|
||||
guard reachability.connection != Reachability.Connection.none else {
|
||||
let error = NSError(domain: "Keyman", code: 0,
|
||||
userInfo: [NSLocalizedDescriptionKey: "No internet connection"])
|
||||
downloadFailed(forBatch: batch, error: error)
|
||||
return false
|
||||
}
|
||||
|
||||
// At this stage, we now have everything needed to generate download requests.
|
||||
guard downloader == nil else {
|
||||
let error = NSError(domain: "Keyman", code: 0,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Download queue is busy"])
|
||||
downloadFailed(forBatch: batch, error: error)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private func execute(_ batch: DownloadBatch) {
|
||||
// TODO: Handling composite batches.
|
||||
downloader = HTTPDownloader(self)
|
||||
downloader!.userInfo = [Key.downloadBatch: batch]
|
||||
|
||||
batch.tasks?.forEach { task in
|
||||
downloader!.addRequest(task.request)
|
||||
}
|
||||
|
||||
//currentBatch = batch
|
||||
queueRoot = DownloadQueueFrame()
|
||||
queueRoot!.nodes.append(batch)
|
||||
downloader!.run()
|
||||
}
|
||||
|
||||
public func queue(_ batch: DownloadBatch) {
|
||||
if queueRoot == nil {
|
||||
execute(batch)
|
||||
} else {
|
||||
queueRoot?.nodes.append(batch)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK - helper methods for ResourceDownloadManager
|
||||
|
||||
var currentRequest: HTTPDownloadRequest? {
|
||||
get {
|
||||
// This was literally the state of this property when this refactor was performed.
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
// Needs validation - I don't think this function is practically utilized (as of 2019-08-16)
|
||||
func keyboardIdForCurrentRequest() -> String? {
|
||||
if let currentRequest = currentRequest {
|
||||
let tmpStr = currentRequest.url.lastPathComponent
|
||||
if tmpStr.hasJavaScriptExtension {
|
||||
return String(tmpStr.dropLast(3))
|
||||
}
|
||||
} else if let downloadQueue = downloader {
|
||||
let kbInfo = downloadQueue.userInfo[Key.keyboardInfo]
|
||||
if let keyboards = kbInfo as? [InstallableKeyboard], let keyboard = keyboards.first {
|
||||
return keyboard.id
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func lexicalModelIdForCurrentRequest() -> String? {
|
||||
if let currentRequest = currentRequest {
|
||||
let tmpStr = currentRequest.url.lastPathComponent
|
||||
if tmpStr.hasJavaScriptExtension {
|
||||
return String(tmpStr.dropLast(3))
|
||||
}
|
||||
} else if let downloadQueue = downloader {
|
||||
let kbInfo = downloadQueue.userInfo[Key.lexicalModelInfo]
|
||||
if let lexicalModels = kbInfo as? [InstallableLexicalModel], let lexicalModel = lexicalModels.first {
|
||||
return lexicalModel.id
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MARK - notification methods
|
||||
|
||||
private func downloadFailed(forBatch batch: DownloadBatch, error: Error) {
|
||||
if batch.activity == .composite {
|
||||
// It's an update operation.
|
||||
// TODO: Needs implementation.
|
||||
} else if batch.type == .keyboard {
|
||||
let keyboards = batch.tasks?.compactMap { task in
|
||||
return task.resources as? [InstallableKeyboard]
|
||||
}.flatMap {$0}
|
||||
downloadFailed(forKeyboards: keyboards ?? [], error: error)
|
||||
} else if batch.type == .lexicalModel {
|
||||
let lexModels = batch.tasks?.compactMap { task in
|
||||
return task.resources as? [InstallableLexicalModel]
|
||||
}.flatMap {$0}
|
||||
downloadFailed(forLanguageID: lexModels![0].languageID, error: error)
|
||||
}
|
||||
}
|
||||
|
||||
public func downloadFailed(forKeyboards keyboards: [InstallableKeyboard], error: Error) {
|
||||
let notification = KeyboardDownloadFailedNotification(keyboards: keyboards, error: error)
|
||||
NotificationCenter.default.post(name: Notifications.keyboardDownloadFailed,
|
||||
object: self,
|
||||
value: notification)
|
||||
}
|
||||
|
||||
public func downloadFailed(forLanguageID languageID: String, error: Error) {
|
||||
let notification = LexicalModelDownloadFailedNotification(lmOrLanguageID: languageID, error: error)
|
||||
NotificationCenter.default.post(name: Notifications.lexicalModelDownloadFailed,
|
||||
object: self,
|
||||
value: notification)
|
||||
}
|
||||
|
||||
public func downloadFailed(forLexicalModelPackage packageURL: String, error: Error) {
|
||||
let notification = LexicalModelDownloadFailedNotification(lmOrLanguageID: packageURL, error: error)
|
||||
NotificationCenter.default.post(name: Notifications.lexicalModelDownloadFailed,
|
||||
object: self,
|
||||
value: notification)
|
||||
}
|
||||
|
||||
public func downloadSucceeded(forLexicalModel lm: InstallableLexicalModel) {
|
||||
let notification = LexicalModelDownloadCompletedNotification([lm])
|
||||
NotificationCenter.default.post(name: Notifications.lexicalModelDownloadCompleted,
|
||||
object: self,
|
||||
value: notification)
|
||||
}
|
||||
|
||||
//MARK: - HTTPDownloadDelegate methods
|
||||
|
||||
func downloadQueueFinished(_ queue: HTTPDownloader) {
|
||||
// We can use the properties of the current "batch" to generate specialized notifications.
|
||||
queueRoot = nil
|
||||
}
|
||||
|
||||
func downloadRequestStarted(_ request: HTTPDownloadRequest) {
|
||||
// If we're downloading a new keyboard.
|
||||
// The extra check is there to filter out other potential request types in the future.
|
||||
if request.tag == 0 {
|
||||
let task = request.userInfo[Key.downloadTask] as! DownloadTask
|
||||
if task.type == .keyboard {
|
||||
NotificationCenter.default.post(name: Notifications.keyboardDownloadStarted,
|
||||
object: self,
|
||||
value: task.resources as! [InstallableKeyboard])
|
||||
} else if task.type == .lexicalModel {
|
||||
NotificationCenter.default.post(name: Notifications.lexicalModelDownloadStarted,
|
||||
object: self,
|
||||
value: task.resources as! [InstallableLexicalModel])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func downloadRequestFinished(_ request: HTTPDownloadRequest) {
|
||||
let batch = request.userInfo[Key.downloadBatch] as! DownloadBatch
|
||||
let isUpdate = batch.activity == .update
|
||||
|
||||
let task = request.userInfo[Key.downloadTask] as! DownloadTask
|
||||
|
||||
// FIXME
|
||||
if let statusCode = request.responseStatusCode, statusCode == 200 {
|
||||
if task.type == .keyboard {
|
||||
// The request has succeeded.
|
||||
if downloader!.requestsCount == 0 {
|
||||
let keyboards = task.resources as? [InstallableKeyboard]
|
||||
|
||||
// Download queue finished.
|
||||
downloader = nil
|
||||
FontManager.shared.registerCustomFonts()
|
||||
log.info("Downloaded keyboard: \(keyboards![0].id).")
|
||||
|
||||
NotificationCenter.default.post(name: Notifications.keyboardDownloadCompleted,
|
||||
object: self,
|
||||
value: keyboards!)
|
||||
// TODO: Trigger by notification. Needs to be done on Manager.swift, not this class.
|
||||
// if isUpdate {
|
||||
// shouldReloadKeyboard = true
|
||||
// inputViewController.reload()
|
||||
// }
|
||||
let userDefaults = Storage.active.userDefaults
|
||||
userDefaults.set([Date()], forKey: Key.synchronizeSWKeyboard)
|
||||
userDefaults.synchronize()
|
||||
}
|
||||
} else if task.type == .lexicalModel {
|
||||
if let lm = installLexicalModelPackage(downloadedPackageFile: URL.init(string: task.request.destinationFile!)!) {
|
||||
downloadSucceeded(forLexicalModel: lm)
|
||||
} else {
|
||||
let installError = NSError(domain: "Keyman", code: 0,
|
||||
userInfo: [NSLocalizedDescriptionKey: "installError"])
|
||||
downloadFailed(forLexicalModelPackage: "\(task.request.url)", error: installError )
|
||||
}
|
||||
|
||||
// Temp - gotta clear the queue to match original behavior for now.
|
||||
if downloader!.requestsCount == 0 {
|
||||
downloader = nil
|
||||
}
|
||||
}
|
||||
} else { // Possible request error (400 Bad Request, 404 Not Found, etc.)
|
||||
downloader!.cancelAllOperations()
|
||||
downloader = nil
|
||||
|
||||
let errorMessage = "\(request.responseStatusMessage ?? ""): \(request.url)"
|
||||
let error = NSError(domain: "Keyman", code: 0,
|
||||
userInfo: [NSLocalizedDescriptionKey: errorMessage])
|
||||
|
||||
if task.type == .keyboard {
|
||||
let keyboards = task.resources as? [InstallableKeyboard]
|
||||
log.error("Keyboard download failed: \(error).")
|
||||
|
||||
if !isUpdate {
|
||||
// Clean up keyboard file if anything fails
|
||||
// TODO: Also clean up remaining fonts
|
||||
try? FileManager.default.removeItem(at: Storage.active.keyboardURL(for: keyboards![0]))
|
||||
}
|
||||
|
||||
downloadFailed(forKeyboards: keyboards ?? [], error: error)
|
||||
} else if task.type == .lexicalModel {
|
||||
let lexicalModels = task.resources as? [InstallableLexicalModel]
|
||||
log.error("Dictionary download failed: \(error).")
|
||||
|
||||
if !isUpdate {
|
||||
// Clean up keyboard file if anything fails
|
||||
// TODO: Also clean up remaining fonts
|
||||
try? FileManager.default.removeItem(at: Storage.active.lexicalModelURL(for: lexicalModels![0]))
|
||||
}
|
||||
|
||||
downloadFailed(forLanguageID: lexicalModels?[0].languageID ?? "", error: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//func downloadRequestSuccess(
|
||||
|
||||
func downloadRequestFailed(_ request: HTTPDownloadRequest) {
|
||||
switch request.typeCode {
|
||||
case .downloadFile:
|
||||
downloader = nil
|
||||
let error = request.error!
|
||||
|
||||
let task = request.userInfo[Key.downloadTask] as! DownloadTask
|
||||
let batch = request.userInfo[Key.downloadBatch] as! DownloadBatch
|
||||
let isUpdate = batch.activity == .update
|
||||
|
||||
if task.type == .keyboard {
|
||||
log.error("Keyboard download failed: \(error).")
|
||||
let keyboards = task.resources as? [InstallableKeyboard]
|
||||
|
||||
if !isUpdate {
|
||||
// Clean up keyboard file if anything fails
|
||||
// TODO: Also clean up remaining fonts
|
||||
try? FileManager.default.removeItem(at: Storage.active.keyboardURL(for: keyboards![0]))
|
||||
}
|
||||
|
||||
downloadFailed(forKeyboards: keyboards ?? [], error: error as NSError)
|
||||
} else if task.type == .lexicalModel {
|
||||
log.error("Dictionary download failed: \(error).")
|
||||
let lexicalModels = task.resources as? [InstallableLexicalModel]
|
||||
|
||||
if !isUpdate {
|
||||
try? FileManager.default.removeItem(at: Storage.active.lexicalModelURL(for: lexicalModels![0]))
|
||||
}
|
||||
|
||||
downloadFailed(forLanguageID: lexicalModels?[0].languageID ?? "", error: error as NSError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK - Language resource installation methods
|
||||
|
||||
// Processes fetched lexical models.
|
||||
// return a lexical model so caller can use it in a downloadSucceeded call
|
||||
// is called by other class funcs
|
||||
func installLexicalModelPackage(downloadedPackageFile: URL) -> InstallableLexicalModel? {
|
||||
var installedLexicalModel: InstallableLexicalModel? = nil
|
||||
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||
var destination = documentsDirectory
|
||||
destination.appendPathComponent("temp/\(downloadedPackageFile.lastPathComponent)")
|
||||
|
||||
KeymanPackage.extract(fileUrl: downloadedPackageFile, destination: destination, complete: { kmp in
|
||||
if let kmp = kmp as! LexicalModelKeymanPackage? {
|
||||
do {
|
||||
try Manager.shared.parseLMKMP(kmp.sourceFolder)
|
||||
log.info("successfully parsed the lexical model in: \(kmp.sourceFolder)")
|
||||
installedLexicalModel = kmp.models[0].installableLexicalModels[0]
|
||||
//this can fail gracefully and not show errors to users
|
||||
try FileManager.default.removeItem(at: downloadedPackageFile)
|
||||
} catch {
|
||||
log.error("Error installing the lexical model: \(error)")
|
||||
}
|
||||
} else {
|
||||
log.error("Error extracting the lexical model from the package: \(KMPError.invalidPackage)")
|
||||
}
|
||||
})
|
||||
return installedLexicalModel
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue