diff --git a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LanguageDetailViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LanguageDetailViewController.swift index cdb14eecbe..4be11f4eb3 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LanguageDetailViewController.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LanguageDetailViewController.swift @@ -70,11 +70,11 @@ class LanguageDetailViewController: UITableViewController, UIAlertViewDelegate { override func viewDidLoad() { super.viewDidLoad() keyboardDownloadStartedObserver = NotificationCenter.default.addObserver( - forName: Notifications.keyboardDownloadStarted, + forName: Notifications.packageDownloadStarted, observer: self, function: LanguageDetailViewController.keyboardDownloadStarted) keyboardDownloadFailedObserver = NotificationCenter.default.addObserver( - forName: Notifications.keyboardDownloadFailed, + forName: Notifications.packageDownloadFailed, observer: self, function: LanguageDetailViewController.keyboardDownloadFailed) log.info("viewDidLoad: LanguageDetailViewController (registered for keyboardDownloadStarted)") diff --git a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LanguageLMDetailViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LanguageLMDetailViewController.swift index ef03c56f9b..e44253c73d 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LanguageLMDetailViewController.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LanguageLMDetailViewController.swift @@ -46,11 +46,11 @@ class LanguageLMDetailViewController: UITableViewController, UIAlertViewDelegate override func viewDidLoad() { super.viewDidLoad() lexicalModelDownloadStartedObserver = NotificationCenter.default.addObserver( - forName: Notifications.lexicalModelDownloadStarted, + forName: Notifications.packageDownloadStarted, observer: self, function: LanguageLMDetailViewController.lexicalModelDownloadStarted) lexicalModelDownloadFailedObserver = NotificationCenter.default.addObserver( - forName: Notifications.lexicalModelDownloadFailed, + forName: Notifications.packageDownloadFailed, observer: self, function: LanguageLMDetailViewController.lexicalModelDownloadFailed) log.info("viewDidLoad: LanguageLMDetailViewController (registered for lexicalModelDownloadStarted)") @@ -143,7 +143,7 @@ class LanguageLMDetailViewController: UITableViewController, UIAlertViewDelegate func downloadHandler(_ lexicalModelIndex: Int) { let package = packages[lexicalModelIndex] let lmFullID = package.0.fullID - let completionClosure: ResourceDownloadManager.CompletionHandler = { package, error in + let completionClosure: ResourceDownloadManager.CompletionHandler = { package, error in ResourceDownloadManager.shared.standardLexicalModelInstallCompletionBlock(forFullID: lmFullID)(package, error) if let lm = package?.findResource(withID: lmFullID) { @@ -151,7 +151,7 @@ class LanguageLMDetailViewController: UITableViewController, UIAlertViewDelegate } } - ResourceDownloadManager.shared.downloadPackage(forFullID: lmFullID, withKey: package.0.packageKey, from: package.1, withNotifications: true, completionBlock: completionClosure) + ResourceDownloadManager.shared.downloadPackage(withKey: package.0.packageKey, from: package.1, withNotifications: true, completionBlock: completionClosure) } private func lexicalModelDownloadStarted() { diff --git a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LanguageViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LanguageViewController.swift index ff0d8084e7..6bd4735320 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LanguageViewController.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LanguageViewController.swift @@ -65,11 +65,11 @@ class LanguageViewController: UITableViewController, UIAlertViewDelegate { title = "Add New Keyboard" selectedSection = NSNotFound keyboardDownloadStartedObserver = NotificationCenter.default.addObserver( - forName: Notifications.keyboardDownloadStarted, + forName: Notifications.packageDownloadStarted, observer: self, function: LanguageViewController.keyboardDownloadStarted) keyboardDownloadFailedObserver = NotificationCenter.default.addObserver( - forName: Notifications.keyboardDownloadFailed, + forName: Notifications.packageDownloadFailed, observer: self, function: LanguageViewController.keyboardDownloadFailed) } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LexicalModelPickerViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LexicalModelPickerViewController.swift index 2d0705eb11..66841a5b5d 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LexicalModelPickerViewController.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LexicalModelPickerViewController.swift @@ -44,15 +44,15 @@ class LexicalModelPickerViewController: UITableViewController, UIAlertViewDelega navigationController?.toolbar?.barTintColor = Colors.statusToolbar lexicalModelDownloadStartedObserver = NotificationCenter.default.addObserver( - forName: Notifications.lexicalModelDownloadStarted, + forName: Notifications.packageDownloadStarted, observer: self, function: LexicalModelPickerViewController.lexicalModelDownloadStarted) lexicalModelDownloadCompletedObserver = NotificationCenter.default.addObserver( - forName: Notifications.lexicalModelDownloadCompleted, + forName: Notifications.packageDownloadCompleted, observer: self, function: LexicalModelPickerViewController.lexicalModelDownloadCompleted) lexicalModelDownloadFailedObserver = NotificationCenter.default.addObserver( - forName: Notifications.lexicalModelDownloadFailed, + forName: Notifications.packageDownloadFailed, observer: self, function: LexicalModelPickerViewController.lexicalModelDownloadFailed) @@ -177,13 +177,13 @@ class LexicalModelPickerViewController: UITableViewController, UIAlertViewDelega return globalIndex } - private func lexicalModelDownloadStarted(_ lexicalModels: [InstallableLexicalModel]) { + private func lexicalModelDownloadStarted() { view.isUserInteractionEnabled = false navigationItem.leftBarButtonItem?.isEnabled = false navigationItem.rightBarButtonItem?.isEnabled = false } - private func lexicalModelDownloadCompleted(_ lexicalModels: [InstallableLexicalModel]) { + private func lexicalModelDownloadCompleted() { log.info("lexicalModelDownloadCompleted LexicalModelPicker") // Actually used now. @@ -196,7 +196,7 @@ class LexicalModelPickerViewController: UITableViewController, UIAlertViewDelega navigationController?.popToRootViewController(animated: true) } - private func lexicalModelDownloadFailed(_ notification: LexicalModelDownloadFailedNotification) { + private func lexicalModelDownloadFailed(_ notification: PackageDownloadFailedNotification) { view.isUserInteractionEnabled = true navigationItem.leftBarButtonItem?.isEnabled = true if let item = navigationItem.rightBarButtonItem { @@ -296,7 +296,7 @@ class LexicalModelPickerViewController: UITableViewController, UIAlertViewDelega if let error = error { log.info("Failed to fetch lexical model list for "+self.language.id+". error: "+error.localizedDescription) DispatchQueue.main.async { - self.lexicalModelDownloadFailed(LexicalModelDownloadFailedNotification(lmOrLanguageID: self.language.id, error: error)) + self.lexicalModelDownloadFailed(PackageDownloadFailedNotification(packageKey: nil, error: error)) } return } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Notification/Notifications.swift b/ios/engine/KMEI/KeymanEngine/Classes/Notification/Notifications.swift index e43cc12d0e..240bb62ae3 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Notification/Notifications.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Notification/Notifications.swift @@ -8,18 +8,33 @@ import Foundation +@available(*, deprecated, message: "Download notifications are now package-based. Use `PackageDownloadStartedNotification` instead.") public typealias KeyboardDownloadStartedNotification = [InstallableKeyboard] +@available(*, deprecated, message: "Download notifications are now package-based. Use `PackageDownloadCompletedNotification` instead.") public typealias KeyboardDownloadCompletedNotification = [InstallableKeyboard] +@available(*, deprecated, message: "Download notifications are now package-based. Use `PackageDownloadFailedNotification` instead.") public struct KeyboardDownloadFailedNotification { public let keyboards: [InstallableKeyboard] public let error: Error } + +@available(*, deprecated, message: "Download notifications are now package-based. Use `PackageDownloadStartedNotification` instead.") public typealias LexicalModelDownloadStartedNotification = [InstallableLexicalModel] +@available(*, deprecated, message: "Download notifications are now package-based. Use `PackageDownloadCompletedNotification` instead.") public typealias LexicalModelDownloadCompletedNotification = [InstallableLexicalModel] +@available(*, deprecated, message: "Download notifications are now package-based. Use `PackageDownloadFailedNotification` instead.") public struct LexicalModelDownloadFailedNotification { public let lmOrLanguageID: String public let error: Error } + +public typealias PackageDownloadStartedNotification = KeymanPackage.Key +public typealias PackageDownloadCompletedNotification = KeymanPackage +public struct PackageDownloadFailedNotification { + public let packageKey: KeymanPackage.Key? + public let error: Error +} + public typealias BatchUpdateStartedNotification = [AnyLanguageResource] public struct BatchUpdateCompletedNotification { @@ -41,10 +56,17 @@ public typealias LexicalModelRemovedNotification = InstallableLexicalModel public typealias LexicalModelPickerDismissedNotification = Void public struct Notifications { + public static let packageDownloadStarted = NotificationName("KeymanPackageDownloadStarted") + public static let packageDownloadCompleted = NotificationName("KeymanPackageDownloadCompleted") + public static let packageDownloadFailed = NotificationName("KeymanPackageDownloadFailed") + + @available(swift, deprecated: 0.1, obsoleted: 0.1, message: "Download notifications are now package-based. Use `packageDownloadStarted` instead.", renamed: "packageDownloadStarted") public static let keyboardDownloadStarted = NotificationName("KeymanKeyboardDownloadStarted") + @available(swift, deprecated: 0.1, obsoleted: 0.1, message: "Download notifications are now package-based. Use `packageDownloadCompleted` instead.", renamed: "packageDownloadCompleted") public static let keyboardDownloadCompleted = NotificationName("KeymanKeyboardDownloadCompleted") + @available(swift, deprecated: 0.1, obsoleted: 0.1, message: "Download notifications are now package-based. Use `packageDownloadFailed` instead.", renamed: "packageDownloadFailed") public static let keyboardDownloadFailed = NotificationName("KeymanKeyboardDownloadFailed") @@ -58,10 +80,13 @@ public struct Notifications { public static let keyboardPickerDismissed = NotificationName("KeymanKeyboardPickerDismissed") + @available(swift, deprecated: 0.1, obsoleted: 0.1, message: "Download notifications are now package-based. Use `packageDownloadStarted` instead.", renamed: "packageDownloadStarted") public static let lexicalModelDownloadStarted = NotificationName("KeymanLexicalModelDownloadStarted") + @available(swift, deprecated: 0.1, obsoleted: 0.1, message: "Download notifications are now package-based. Use `packageDownloadCompleted` instead.", renamed: "packageDownloadCompleted") public static let lexicalModelDownloadCompleted = NotificationName("KeymanLexicalModelDownloadCompleted") + @available(swift, deprecated: 0.1, obsoleted: 0.1, message: "Download notifications are now package-based. Use `packageDownloadFailed` instead.", renamed: "packageDownloadFailed") public static let lexicalModelDownloadFailed = NotificationName("KeymanLexicalModelDownloadFailed") diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceDownloadManager.swift b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceDownloadManager.swift index d1147bd712..576b5bfc1e 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceDownloadManager.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceDownloadManager.swift @@ -22,7 +22,7 @@ public class ResourceDownloadManager { public static let DISTRIBUTION_CACHE_VALIDITY_THRESHOLD = TimeInterval(60*24*7) // in seconds. 60 minutes, 24 hrs, 7 days. - public typealias CompletionHandler = (Resource.Package?, Error?) -> Void where Resource.Package: TypedKeymanPackage + public typealias CompletionHandler = (Package?, Error?) -> Void public typealias BatchCompletionHandler = () -> Void internal typealias InternalBatchCompletionHandler = (CompositeBatch) -> Void @@ -39,41 +39,12 @@ public class ResourceDownloadManager { downloader = ResourceDownloadQueue(session: session, autoExecute: autoExecute) } - // MARK: - Common functionality - - private func fetchHandler(for resourceType: LanguageResourceType?, _ 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.resourceDownloadFailed(for: [] as [InstallableKeyboard], with: error) - } else { - log.info("Fetched repository. Continuing with download.") - completionHandler() - } - } - } - - // MARK - Downloading keyboards - - private func getInstallableKeyboardMetadata(withID keyboardID: String, languageID: String) -> InstallableKeyboard? { - // Grab info for the relevant API version of the keyboard. - guard let keyboard = Manager.shared.apiKeyboardRepository.installableKeyboard(withID: keyboardID, languageID: languageID) - else { - let message = "Keyboard not found with id: \(keyboardID), languageID: \(languageID)" - let error = NSError(domain: "Keyman", code: 0, - userInfo: [NSLocalizedDescriptionKey: message]) - self.resourceDownloadFailed(for: [] as [InstallableKeyboard], with: error) - return nil - } - - return keyboard - } + // MARK - Downloading resources // Used to maintain legacy API: downloadKeyboard and downloadLexicalModel (based on ID, language ID) private func downloadResource(withFullID fullID: FullID, sendNotifications: Bool, - completionBlock: CompletionHandler?) + completionBlock: CompletionHandler?) where FullID.Resource.Package: TypedKeymanPackage { // Note: in this case, someone knows the "full ID" of the resource already, but NOT its location. // We can use the package-version query to attempt a lookup for a .kmp location @@ -82,7 +53,7 @@ public class ResourceDownloadManager { Queries.PackageVersion.fetch(for: [packageKey], withSession: session) { result, error in guard let result = result, error == nil else { log.info("Error occurred requesting location for \(fullID.description)") - self.resourceDownloadFailed(forFullID: fullID, with: error ?? .noData) + self.resourceDownloadFailed(withKey: packageKey, with: error ?? .noData) completionBlock?(nil, error ?? .noData) return } @@ -93,7 +64,7 @@ public class ResourceDownloadManager { log.info("Query reported error: \(String(describing: errorEntry.error))") } } - self.resourceDownloadFailed(forFullID: fullID, with: Queries.ResultError.unqueried) + self.resourceDownloadFailed(withKey: packageKey, with: Queries.ResultError.unqueried) completionBlock?(nil, Queries.ResultError.unqueried) return } @@ -102,20 +73,19 @@ public class ResourceDownloadManager { guard self.downloader.state == .clear else { let err = self.downloader.state.error ?? NSError(domain: "Keyman", code: 0, userInfo: [NSLocalizedDescriptionKey: "Already busy downloading something"]) - self.resourceDownloadFailed(forFullID: fullID, with: err) + self.resourceDownloadFailed(withKey: packageKey, with: err) completionBlock?(nil, err) return } - let completionClosure: CompletionHandler = completionBlock ?? { package, error in + let completionClosure: CompletionHandler = completionBlock ?? { package, error in // If the caller doesn't specify a completion block, this will carry out a default installation. if let package = package { try? ResourceFileManager.shared.install(resourceWithID: fullID, from: package) } } - self.downloadPackage(forFullID: fullID, - withKey: packageKey, + self.downloadPackage(withKey: packageKey, from: URL.init(string: data.packageURL)!, withNotifications: sendNotifications, completionBlock: completionClosure) @@ -131,7 +101,7 @@ public class ResourceDownloadManager { languageID: String, isUpdate: Bool, fetchRepositoryIfNeeded: Bool = true, - completionBlock: CompletionHandler? = nil) { + completionBlock: CompletionHandler? = nil) { let kbdFullID = FullKeyboardID(keyboardID: keyboardID, languageID: languageID) downloadResource(withFullID: kbdFullID, sendNotifications: !isUpdate, completionBlock: completionBlock) } @@ -205,8 +175,8 @@ public class ResourceDownloadManager { // The original lacked this check, as well, and it's a bit of an edge case right now. Queries.LexicalModel.fetch(forLanguageCode: languageID) { result, error in if let error = error { + // We never quite started downloading the lexical model, so there's no download to have failed. log.info("Failed to fetch lexical model list for "+languageID+". error: "+error.localizedDescription) - self.resourceDownloadFailed(for: [] as [InstallableLexicalModel], with: error) return } @@ -222,8 +192,7 @@ public class ResourceDownloadManager { } else if let lmFullID = result[0].modelFor(languageID: languageID)?.fullID { log.info("Fetched lexical model list for "+languageID+".") let completionClosure = self.standardLexicalModelInstallCompletionBlock(forFullID: lmFullID) - self.downloadPackage(forFullID: lmFullID, - withKey: KeymanPackage.Key(id: lmFullID.id, type: .lexicalModel), + self.downloadPackage(withKey: KeymanPackage.Key(id: lmFullID.id, type: .lexicalModel), from: URL.init(string: result[0].packageFilename)!, completionBlock: completionClosure) } @@ -239,7 +208,7 @@ public class ResourceDownloadManager { languageID: String, isUpdate: Bool, fetchRepositoryIfNeeded: Bool = true, - completionBlock: CompletionHandler? = nil) { + completionBlock: CompletionHandler? = nil) { // Note: in this case, someone knows the "full ID" of the model already, but NOT its location. // For lexical models, we can use either the LexicalModel query or the PackageVersion query. // For consistency with keyboard download behavior, we use the PackageVersion query here. @@ -283,14 +252,11 @@ public class ResourceDownloadManager { * * `withNotifications` specifies whether or not any of KeymanEngine's `NotificationCenter` notifications should be generated. */ - public func downloadPackage(forFullID fullID: FullID, - withKey packageKey: KeymanPackage.Key, - from url: URL, - withNotifications: Bool = false, - completionBlock: @escaping CompletionHandler) - where FullID.Resource.Package: TypedKeymanPackage { - let batch = buildPackageBatch(forFullID: fullID, - withKey: packageKey, + public func downloadPackage(withKey packageKey: KeymanPackage.Key, + from url: URL, + withNotifications: Bool = false, + completionBlock: @escaping CompletionHandler) { + let batch = buildPackageBatch(withKey: packageKey, from: url, withNotifications: withNotifications, completionBlock: completionBlock) @@ -299,21 +265,17 @@ public class ResourceDownloadManager { // Facilitates re-use of the downloadPackage core for updates. // Also allows specifying LanguageResource instances for use in notifications. - internal func buildPackageBatch(forFullID fullID: FullID, - withKey packageKey: KeymanPackage.Key, - from url: URL, - withNotifications: Bool = false, - withResource resource: FullID.Resource? = nil, - completionBlock: @escaping CompletionHandler) -> DownloadBatch - where FullID.Resource.Package: TypedKeymanPackage { + internal func buildPackageBatch(withKey packageKey: KeymanPackage.Key, + from url: URL, + withNotifications: Bool = false, + withResource resource: AnyLanguageResource? = nil, + completionBlock: @escaping CompletionHandler) -> DownloadBatch { var startClosure: (() -> Void)? = nil - var completionClosure: CompletionHandler? = completionBlock + var completionClosure: CompletionHandler? = completionBlock if withNotifications { - let resources = resource != nil ? [resource!] : [] as [FullID.Resource] - // We don't have the full metadata available, but we can at least signal which resource type this way. - startClosure = resourceDownloadStartClosure(forFullID: fullID) - completionClosure = resourceDownloadCompletionClosure(for: resources, handler: completionBlock) + startClosure = resourceDownloadStartClosure(withKey: packageKey) + completionClosure = resourceDownloadCompletionClosure(withKey: packageKey, handler: completionBlock) } // build batch for package @@ -455,7 +417,7 @@ public class ResourceDownloadManager { * You know, once it's written. */ - // TODO: Keyboard update is broken, as apiKeyboardRepository will specify the wrong file. + // TODO: All package updates are currently broken, as apiKeyboardRepository will specify the wrong file. // TODO: Merge the keyboard and lexical model pathways; it's WET code. resources.forEach { res in if let kbd = res as? InstallableKeyboard { @@ -470,16 +432,16 @@ public class ResourceDownloadManager { // batches.append(batch) // } } else if let lex = res as? InstallableLexicalModel { - if let filename = Manager.shared.apiLexicalModelRepository.lexicalModels?[lex.id]?.packageFilename, - let path = URL.init(string: filename) { - let batch = self.buildPackageBatch(forFullID: lex.fullID, withKey: lex.packageKey, from: path, withResource: lex) { package, error in - if let package = package { - try? ResourceFileManager.shared.install(resourceWithID: lex.fullID, from: package) - } - // else error: already handled by wrapping closure set within buildPackageBatch. - } - batches.append(batch) - } +// if let filename = Manager.shared.apiLexicalModelRepository.lexicalModels?[lex.id]?.packageFilename, +// let path = URL.init(string: filename) { +// let batch = self.buildPackageBatch(withKey: lex.packageKey, from: path, withResource: lex) { package, error in +// if let package = package { +// try? ResourceFileManager.shared.install(resourceWithID: lex.fullID, from: package) +// } +// // else error: already handled by wrapping closure set within buildPackageBatch. +// } +// batches.append(batch) +// } } } @@ -508,33 +470,18 @@ public class ResourceDownloadManager { // MARK - Completion handlers. - internal func resourceDownloadStartClosure(for resources: [Resource]) -> (() -> Void) { - return { self.resourceDownloadStarted(for: resources) } - } - - internal func resourceDownloadStartClosure(forFullID fullID: FullID) -> (() -> Void) { - return { self.resourceDownloadStarted(forFullID: fullID) } + internal func resourceDownloadStartClosure(withKey packageKey: KeymanPackage.Key) -> (() -> Void) { + return { self.resourceDownloadStarted(withKey: packageKey) } } // Only for use with individual downloads. Updates should have different completion handling. - internal func resourceDownloadCompletionClosure(for resources: [Resource], handler: CompletionHandler?) -> CompletionHandler { + internal func resourceDownloadCompletionClosure(withKey packageKey: KeymanPackage.Key, handler: CompletionHandler?) -> CompletionHandler { return { package, error in if let error = error { - resources.forEach { resource in - do { - let resourcePath = Storage.active.resourceURL(for: resource)! - if FileManager.default.fileExists(atPath: resourcePath.path) { - try? FileManager.default.removeItem(at: resourcePath) - } - } - } - - self.resourceDownloadFailed(for: resources, with: error) - } else if let _ = package { + self.resourceDownloadFailed(withKey: packageKey, with: error) + } else if let package = package { // successful download - // Problem: this uses the lookup-version of the resources, which may not be perfect matches - // for what lies within the newly-downloaded package. - self.resourceDownloadCompleted(for: resources) + self.resourceDownloadCompleted(with: package) } handler?(package, error) @@ -547,42 +494,6 @@ public class ResourceDownloadManager { } } - internal func resourceUpdateCompletionClosure(for resources: [Resource]) -> CompletionHandler { - // Updates should not generate notifications per resource. - return { package, error in - if let package = package { - // Do not send notifications for individual resource updates. - let resourceIDs: [Resource.FullID] = resources.map { $0.typedFullID } - do { - if let keyboards = resources as? [InstallableKeyboard], let package = package as? KeyboardKeymanPackage { - // TEMP: currently required because downloaded keyboards aren't actually in packages. - keyboards.forEach { keyboard in - if let updatedKbd = package.findResource(withID: keyboard.typedFullID) { - Manager.shared.updateUserKeyboards(with: updatedKbd) - - if Manager.shared.currentKeyboard?.fullID == keyboard.fullID { - // Issue: does not actually trigger a reload if the user isn't within the Settings view hierarchy - // Fixing this requires a refactor of `shouldReloadKeyboard`. - Manager.shared.shouldReloadKeyboard = true - } - } - } - } else if let _ = resources as? [InstallableLexicalModel] { - try ResourceFileManager.shared.install(resourcesWithIDs: resourceIDs, from: package) - } - } catch { - log.error("Error updating resources from package \(package.id)") - } - - // After the custom handler operates, ensure that any changes it made are synchronized for use - // with the app extension, too. - let userDefaults = Storage.active.userDefaults - userDefaults.set([Date()], forKey: Key.synchronizeSWKeyboard) - userDefaults.synchronize() - } - } - } - internal func resourceBatchUpdateStartClosure(for resources: [AnyLanguageResource]) -> (() -> Void) { return { let notification = BatchUpdateStartedNotification(resources) @@ -619,7 +530,7 @@ public class ResourceDownloadManager { } } - public func standardKeyboardInstallCompletionBlock(forFullID fullID: FullKeyboardID, withModel: Bool = true) -> CompletionHandler { + public func standardKeyboardInstallCompletionBlock(forFullID fullID: FullKeyboardID, withModel: Bool = true) -> CompletionHandler { return { package, error in if let package = package { do { @@ -645,7 +556,7 @@ public class ResourceDownloadManager { } } - public func standardLexicalModelInstallCompletionBlock(forFullID fullID: FullLexicalModelID) -> CompletionHandler { + public func standardLexicalModelInstallCompletionBlock(forFullID fullID: FullLexicalModelID) -> CompletionHandler { return { package, error in if let package = package { do { @@ -668,73 +579,21 @@ public class ResourceDownloadManager { } // MARK - Notifications - internal func resourceDownloadStarted(for resources: [Resource]) { - if let keyboards = resources as? [InstallableKeyboard] { - NotificationCenter.default.post(name: Notifications.keyboardDownloadStarted, - object: self, - value: keyboards) - } else if let lexicalModels = resources as? [InstallableLexicalModel] { - NotificationCenter.default.post(name: Notifications.lexicalModelDownloadStarted, - object: self, - value: lexicalModels) - } + internal func resourceDownloadStarted(withKey packageKey: KeymanPackage.Key) { + NotificationCenter.default.post(name: Notifications.packageDownloadStarted, + object: self, + value: packageKey) } - internal func resourceDownloadStarted(forFullID id: FullID) { - // Note: when all is said and done, we may want to rework notifications to report the FullID, not - // the full LanguageResource. - if let _ = id as? FullKeyboardID { - NotificationCenter.default.post(name: Notifications.keyboardDownloadStarted, - object: self, - value: []) - } else if let _ = id as? FullLexicalModelID { - NotificationCenter.default.post(name: Notifications.lexicalModelDownloadStarted, - object: self, - value: []) - } + internal func resourceDownloadCompleted(with package: KeymanPackage) { + NotificationCenter.default.post(name: Notifications.packageDownloadCompleted, + object: self, + value: package) } - internal func resourceDownloadCompleted(for resources: [Resource]) { - if let keyboards = resources as? [InstallableKeyboard] { - let notification = KeyboardDownloadCompletedNotification(keyboards) - NotificationCenter.default.post(name: Notifications.keyboardDownloadCompleted, - object: self, - value: notification) - } else if let lexicalModels = resources as? [InstallableLexicalModel] { - let notification = LexicalModelDownloadCompletedNotification(lexicalModels) - NotificationCenter.default.post(name: Notifications.lexicalModelDownloadCompleted, - object: self, - value: notification) - } - } - - internal func resourceDownloadFailed(for resources: [Resource], with error: Error) { - if let keyboards = resources as? [InstallableKeyboard] { - let notification = KeyboardDownloadFailedNotification(keyboards: keyboards, error: error) - NotificationCenter.default.post(name: Notifications.keyboardDownloadFailed, - object: self, - value: notification) - } else if let lexicalModels = resources as? [InstallableLexicalModel] { - // Sadly, this notification reports with a different format. - let languageID = lexicalModels.count > 0 ? lexicalModels[0].languageID : "" - let notification = LexicalModelDownloadFailedNotification(lmOrLanguageID: languageID, error: error) - NotificationCenter.default.post(name: Notifications.lexicalModelDownloadFailed, - object: self, - value: notification) - } - } - - internal func resourceDownloadFailed(forFullID fullID: FullID, with error: Error) { - if let _ = fullID as? FullKeyboardID { - let notification = KeyboardDownloadFailedNotification(keyboards: [], error: error) - NotificationCenter.default.post(name: Notifications.keyboardDownloadFailed, - object: self, - value: notification) - } else if let _ = fullID as? FullLexicalModelID { - let notification = LexicalModelDownloadFailedNotification(lmOrLanguageID: fullID.languageID, error: error) - NotificationCenter.default.post(name: Notifications.lexicalModelDownloadFailed, - object: self, - value: notification) - } + internal func resourceDownloadFailed(withKey packageKey: KeymanPackage.Key, with error: Error) { + NotificationCenter.default.post(name: Notifications.packageDownloadFailed, + object: self, + value: PackageDownloadFailedNotification(packageKey: packageKey, error: error)) } } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceDownloadQueue.swift b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceDownloadQueue.swift index 51773f4ac8..48605ffcc9 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceDownloadQueue.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceDownloadQueue.swift @@ -123,17 +123,17 @@ protocol AnyDownloadBatch { /** * Represents one overall resource-related command for requests against the Keyman Cloud API. */ -class DownloadBatch: AnyDownloadBatch where FullID.Resource.Package: TypedKeymanPackage { +class DownloadBatch: AnyDownloadBatch { typealias CompletionHandler = ResourceDownloadManager.CompletionHandler public final var downloadTasks: [DownloadTask] var errors: [Error?] // Only used by the ResourceDownloadQueue. public final var startBlock: (() -> Void)? = nil - public final var completionBlock: CompletionHandler? = nil + public final var completionBlock: CompletionHandler? = nil public init?(do tasks: [DownloadTask], startBlock: (() -> Void)? = nil, - completionBlock: CompletionHandler? = nil) { + completionBlock: CompletionHandler? = nil) { self.downloadTasks = tasks self.errors = Array(repeating: nil, count: tasks.count) @@ -144,7 +144,7 @@ class DownloadBatch: AnyDownloadBatch where Full public init(forPackageWithKey packageKey: KeymanPackage.Key, from url: URL, startBlock: (() -> Void)?, - completionBlock: CompletionHandler?) { + completionBlock: CompletionHandler?) { // If we can't build a proper DownloadTask, we can't build the batch. let tempArtifact = ResourceFileManager.shared.packageDownloadTempPath(forKey: packageKey) let finalFile = ResourceFileManager.shared.cachedPackagePath(forKey: packageKey) @@ -189,7 +189,7 @@ class DownloadBatch: AnyDownloadBatch where Full completionBlock = nil do { - if let package = try ResourceFileManager.shared.prepareKMPInstall(from: file) as? FullID.Resource.Package { + if let package = try ResourceFileManager.shared.prepareKMPInstall(from: file) as? Package { complete?(package, nil) } else { complete?(nil, KMPError.invalidPackage) diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Settings/InstalledLanguagesViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/Settings/InstalledLanguagesViewController.swift index 8bfa793f35..b57c2cdae2 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Settings/InstalledLanguagesViewController.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Settings/InstalledLanguagesViewController.swift @@ -27,12 +27,9 @@ public class InstalledLanguagesViewController: UITableViewController, UIAlertVie private var isDidUpdateCheck = false - private var keyboardDownloadStartedObserver: NotificationObserver? - private var keyboardDownloadCompletedObserver: NotificationObserver? - private var keyboardDownloadFailedObserver: NotificationObserver? - private var lexicalModelDownloadStartedObserver: NotificationObserver? - private var lexicalModelDownloadCompletedObserver: NotificationObserver? - private var lexicalModelDownloadFailedObserver: NotificationObserver? + private var packageDownloadStartedObserver: NotificationObserver? + private var packageDownloadCompletedObserver: NotificationObserver? + private var packageDownloadFailedObserver: NotificationObserver? private var batchUpdateStartedObserver: NotificationObserver? private var batchUpdateCompletedObserver: NotificationObserver? @@ -62,31 +59,18 @@ public class InstalledLanguagesViewController: UITableViewController, UIAlertVie title = "Installed Languages" selectedSection = NSNotFound - keyboardDownloadStartedObserver = NotificationCenter.default.addObserver( - forName: Notifications.keyboardDownloadStarted, + packageDownloadStartedObserver = NotificationCenter.default.addObserver( + forName: Notifications.packageDownloadStarted, observer: self, - function: InstalledLanguagesViewController.keyboardDownloadStarted) - keyboardDownloadCompletedObserver = NotificationCenter.default.addObserver( - forName: Notifications.keyboardDownloadCompleted, + function: InstalledLanguagesViewController.packageDownloadStarted) + packageDownloadCompletedObserver = NotificationCenter.default.addObserver( + forName: Notifications.packageDownloadCompleted, observer: self, - function: InstalledLanguagesViewController.keyboardDownloadCompleted) - keyboardDownloadFailedObserver = NotificationCenter.default.addObserver( - forName: Notifications.keyboardDownloadFailed, + function: InstalledLanguagesViewController.packageDownloadCompleted) + packageDownloadFailedObserver = NotificationCenter.default.addObserver( + forName: Notifications.packageDownloadFailed, observer: self, - function: InstalledLanguagesViewController.keyboardDownloadFailed) - - lexicalModelDownloadStartedObserver = NotificationCenter.default.addObserver( - forName: Notifications.lexicalModelDownloadStarted, - observer: self, - function: InstalledLanguagesViewController.lexicalModelDownloadStarted) - lexicalModelDownloadCompletedObserver = NotificationCenter.default.addObserver( - forName: Notifications.lexicalModelDownloadCompleted, - observer: self, - function: InstalledLanguagesViewController.lexicalModelDownloadCompleted) - lexicalModelDownloadFailedObserver = NotificationCenter.default.addObserver( - forName: Notifications.lexicalModelDownloadFailed, - observer: self, - function: InstalledLanguagesViewController.lexicalModelDownloadFailed) + function: InstalledLanguagesViewController.packageDownloadFailed) batchUpdateStartedObserver = NotificationCenter.default.addObserver( forName: Notifications.batchUpdateStarted, @@ -293,6 +277,37 @@ public class InstalledLanguagesViewController: UITableViewController, UIAlertVie toolbar.displayStatus("Downloading \(downloadLabel)\u{2026}", withIndicator: true) } + + private func packageDownloadStarted(key: KeymanPackage.Key) { + switch(key.type) { + case .keyboard: + keyboardDownloadStarted() + case .lexicalModel: + lexicalModelDownloadStarted() + } + } + + private func packageDownloadCompleted(package: KeymanPackage) { + switch(package.resourceType()) { + case .keyboard: + keyboardDownloadCompleted() + case .lexicalModel: + lexicalModelDownloadCompleted() + } + } + + private func packageDownloadFailed(notification: PackageDownloadFailedNotification) { + guard let packageKey = notification.packageKey else { + return + } + + switch(packageKey.type) { + case .keyboard: + keyboardDownloadFailed() + case .lexicalModel: + lexicalModelDownloadFailed() + } + } private func keyboardDownloadStarted() { log.info("keyboardDownloadStarted: InstalledLanguagesViewController") @@ -301,14 +316,14 @@ public class InstalledLanguagesViewController: UITableViewController, UIAlertVie showDownloading("keyboard") } - private func lexicalModelDownloadStarted(_ lexicalModels: [InstallableLexicalModel]) { + private func lexicalModelDownloadStarted() { log.info("lexicalModelDownloadStarted") view.isUserInteractionEnabled = false navigationItem.setHidesBackButton(true, animated: true) showDownloading("dictionary") } - private func keyboardDownloadCompleted(_ keyboards: [InstallableKeyboard]) { + private func keyboardDownloadCompleted() { log.info("keyboardDownloadCompleted: InstalledLanguagesViewController") Manager.shared.shouldReloadKeyboard = true @@ -320,7 +335,7 @@ public class InstalledLanguagesViewController: UITableViewController, UIAlertVie navigationController?.popToRootViewController(animated: true) } - private func lexicalModelDownloadCompleted(_ lexicalModels: [InstallableLexicalModel]) { + private func lexicalModelDownloadCompleted() { log.info("lexicalModelDownloadCompleted: InstalledLanguagesViewController") if let toolbar = navigationController?.toolbar as? ResourceDownloadStatusToolbar { diff --git a/ios/engine/KMEI/KeymanEngineTests/FileManagementTests.swift b/ios/engine/KMEI/KeymanEngineTests/FileManagementTests.swift index b2f09e162c..932eee618d 100644 --- a/ios/engine/KMEI/KeymanEngineTests/FileManagementTests.swift +++ b/ios/engine/KMEI/KeymanEngineTests/FileManagementTests.swift @@ -170,8 +170,7 @@ class FileManagementTests: XCTestCase { // State 2: Download requested. // Note - we don't need to actually run the queue. let downloadManager = ResourceDownloadManager(session: TestUtils.Downloading.URLSessionMock(), autoExecute: false) - downloadManager.downloadPackage(forFullID: TestUtils.Keyboards.khmer_angkor.fullID, - withKey: khmer_angkor_key, + downloadManager.downloadPackage(withKey: khmer_angkor_key, from: TestUtils.Keyboards.khmerAngkorKMP, completionBlock: { _, _ in }) diff --git a/ios/engine/KMEI/KeymanEngineTests/ResourceDownloadManagerTests.swift b/ios/engine/KMEI/KeymanEngineTests/ResourceDownloadManagerTests.swift index 1508e6eddf..a21ffbe4f0 100644 --- a/ios/engine/KMEI/KeymanEngineTests/ResourceDownloadManagerTests.swift +++ b/ios/engine/KMEI/KeymanEngineTests/ResourceDownloadManagerTests.swift @@ -34,14 +34,12 @@ class ResourceDownloadManagerTests: XCTestCase { func testDownloadPackageForKeyboard() throws { let expectation = XCTestExpectation(description: "Mocked \"download\" should complete successfully.") - let khmer_angkor_id = TestUtils.Keyboards.khmer_angkor.fullID let packageKey = TestUtils.Keyboards.khmer_angkor.packageKey let mockedResult = TestUtils.Downloading.MockResult(location: TestUtils.Keyboards.khmerAngkorKMP, error: nil) mockedURLSession?.queueMockResult(.download(mockedResult)) - downloadManager?.downloadPackage(forFullID: khmer_angkor_id, withKey: packageKey, from: TestUtils.Keyboards.khmerAngkorKMP, withNotifications: false) { package, error in - + downloadManager?.downloadPackage(withKey: packageKey, from: TestUtils.Keyboards.khmerAngkorKMP, withNotifications: false) { (package: KeyboardKeymanPackage?, error: Error?) in let tempDownloadKMP = ResourceFileManager.shared.packageDownloadTempPath(forKey: packageKey) XCTAssertFalse(FileManager.default.fileExists(atPath: tempDownloadKMP.path)) @@ -71,14 +69,12 @@ class ResourceDownloadManagerTests: XCTestCase { func testDownloadPackageForLexicalModel() throws { let expectation = XCTestExpectation(description: "Mocked \"download\" should complete successfully.") - let mtnt_id = TestUtils.LexicalModels.mtnt.fullID let packageKey = TestUtils.LexicalModels.mtnt.packageKey let mockedResult = TestUtils.Downloading.MockResult(location: TestUtils.LexicalModels.mtntKMP, error: nil) mockedURLSession?.queueMockResult(.download(mockedResult)) - downloadManager?.downloadPackage(forFullID: mtnt_id, withKey: packageKey, from: TestUtils.LexicalModels.mtntKMP, withNotifications: false) { package, error in - + downloadManager?.downloadPackage(withKey: packageKey, from: TestUtils.LexicalModels.mtntKMP, withNotifications: false) { (package: LexicalModelKeymanPackage?, error: Error?) in let tempDownloadKMP = ResourceFileManager.shared.packageDownloadTempPath(forKey: packageKey) XCTAssertFalse(FileManager.default.fileExists(atPath: tempDownloadKMP.path)) @@ -107,13 +103,12 @@ class ResourceDownloadManagerTests: XCTestCase { func testDownloadPackageFailure() throws { let expectation = XCTestExpectation(description: "Mocked \"download\" should complete, though with an error.") - let khmer_angkor_id = TestUtils.Keyboards.khmer_angkor.fullID let packageKey = TestUtils.Keyboards.khmer_angkor.packageKey let mockedResult = TestUtils.Downloading.MockResult(location: TestUtils.Keyboards.khmerAngkorKMP, error: TestUtils.mockedError) mockedURLSession?.queueMockResult(.download(mockedResult)) - downloadManager?.downloadPackage(forFullID: khmer_angkor_id, withKey: packageKey, from: TestUtils.Keyboards.khmerAngkorKMP, withNotifications: false) { package, error in + downloadManager?.downloadPackage(withKey: packageKey, from: TestUtils.Keyboards.khmerAngkorKMP, withNotifications: false) { package, error in let tempDownloadKMP = ResourceFileManager.shared.packageDownloadTempPath(forKey: packageKey) XCTAssertFalse(FileManager.default.fileExists(atPath: tempDownloadKMP.path)) @@ -208,10 +203,9 @@ class ResourceDownloadManagerTests: XCTestCase { XCTAssertEqual(downloadManager!.stateForKeyboard(withID: khmer_angkor_id.id), .needsDownload) - downloadManager!.downloadPackage(forFullID: khmer_angkor_id, - withKey: packageKey, + downloadManager!.downloadPackage(withKey: packageKey, from: TestUtils.Keyboards.khmerAngkorKMP, - withNotifications: false) { package, error in + withNotifications: false) { (package: KeyboardKeymanPackage?, error) in if let _ = error { XCTFail() baseInstallation.fulfill()