diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift b/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift index d6c8ddd19b..571598e911 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift @@ -617,60 +617,11 @@ public class Manager: NSObject, UIGestureRecognizerDelegate { throw KMPError.wrongPackageType } - for r in kmp.resources { - let installableFiles: [(LanguageResource, [(String, URL)])] = r.installableResources.map { resource in - // (source file, destination file) - var set: [(String, URL)] = [(resource.sourceFilename, Storage.active.resourceURL(for: resource)!)] - let installableFonts: [(String, URL)] = resource.fonts.map { font in - // KMPs only list a single font file for each entry whenever one is included. - // A pre-existing assumption. - let fontFile = font.source[0] - return (fontFile, Storage.active.fontURL(forResource: resource, filename: fontFile)!) - } - - set.append(contentsOf: installableFonts) - - // (resource, source-destination file mapping) - return (resource, set) - } - - var haveInstalledOne = false - for set in installableFiles { - let resource = set.0 - let files = set.1 - - do { - try FileManager.default.createDirectory(at: Storage.active.resourceDir(for: resource)!, - withIntermediateDirectories: true) - } catch { - log.error("Could not create dir for download: \(error)") - throw KMPError.fileSystem - } - - do { - for item in files { - var filePath = folder - filePath.appendPathComponent(item.0) - - // TODO: The rest of this block may be replaced with ResourceFileManager.copyWithOverwrite - // once this method is fully abstracted and its core is placed within said class. - if(FileManager.default.fileExists(atPath: (item.1).path)) { - try FileManager.default.removeItem(at: item.1) - } - try FileManager.default.copyItem(at: filePath, to: item.1) - - } - } catch { - log.error("Error saving the download: \(error)") - throw KMPError.copyFiles - } - - // Only one keyboard is installed by default from a KMP. - // Also, only the first language-pairing it contains - if !haveInstalledOne { - Manager.shared.addKeyboard(resource as! InstallableKeyboard) - haveInstalledOne = true - } + for resourceSet in kmp.installableResourceSets { + for resource in resourceSet { + try ResourceFileManager.shared.install(resource, from: kmp) + // Install the keyboard for only the first language pairing defined in the package. + break } } } @@ -681,48 +632,9 @@ public class Manager: NSObject, UIGestureRecognizerDelegate { throw KMPError.wrongPackageType } - for r in kmp.resources { - let installableFiles: [(LanguageResource, [(String, URL)])] = r.installableResources.map { resource in - // (source file, destination file) - let set: [(String, URL)] = [(resource.sourceFilename, Storage.active.resourceURL(for: resource)!)] - // no fonts for lexical models - - // (resource, source-destination file mapping) - return (resource, set) - } - - for set in installableFiles { - let resource = set.0 - let files = set.1 - - do { - try FileManager.default.createDirectory(at: Storage.active.resourceDir(for: resource)!, - withIntermediateDirectories: true) - } catch { - log.error("Could not create dir for download: \(error)") - throw KMPError.fileSystem - } - - do { - for item in files { - var filePath = folder - filePath.appendPathComponent(item.0) - - // TODO: The rest of this block may be replaced with ResourceFileManager.copyWithOverwrite - // once this method is fully abstracted and its core is placed within said class. - if(FileManager.default.fileExists(atPath: (item.1).path)) { - try FileManager.default.removeItem(at: item.1) - } - try FileManager.default.copyItem(at: filePath, to: item.1) - } - } catch { - log.error("Error saving the lexical model download: \(error)") - throw KMPError.copyFiles - } - - // All pairings are installed for lexical models. - // Also, all models within the KMP - Manager.addLexicalModel(resource as! InstallableLexicalModel) + try kmp.installableResourceSets.forEach { resourceSet in + try resourceSet.forEach { resource in + try ResourceFileManager.shared.install(resource, from: kmp) } } } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift index b9f1098568..991603d842 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift @@ -178,4 +178,51 @@ public class ResourceFileManager { completionHandler(error) } } + + public func install(_ resource: LanguageResource, from package: KeymanPackage) throws { + // Ideally, we wouldn't need the guard... but it's best to check just in case. + guard package.contains(resource) else { + throw KMPError.resourceNotInPackage + } + + // (source, destination) + var installableFiles: [(String, URL)] = [(resource.sourceFilename, Storage.active.resourceURL(for: resource)!)] + let installableFonts: [(String, URL)] = resource.fonts.map { font in + // KMPs only list a single font file for each entry whenever one is included. + // A pre-existing assumption. + let fontFile = font.source[0] + return (fontFile, Storage.active.fontURL(forResource: resource, filename: fontFile)!) + } + + installableFiles.append(contentsOf: installableFonts) + + do { + try FileManager.default.createDirectory(at: Storage.active.resourceDir(for: resource)!, + withIntermediateDirectories: true) + } catch { + log.error("Could not create installation directory: \(error)") + throw KMPError.fileSystem + } + + do { + for item in installableFiles { + var filePath = package.sourceFolder + filePath.appendPathComponent(item.0) + try copyWithOverwrite(from: filePath, to: item.1) + } + } catch { + log.error("Error installing the resource: \(error)") + throw KMPError.copyFiles + } + + // There's no generalized method for this quite yet. Manager doesn't need even + // more of these. + if let keyboard = resource as? InstallableKeyboard { + Manager.shared.addKeyboard(keyboard) + } else if let lexicalModel = resource as? InstallableLexicalModel { + Manager.addLexicalModel(lexicalModel) + } else { + fatalError("Cannot install instance of unexpected LanguageResource subclass") + } + } } diff --git a/ios/engine/KMEI/KeymanEngineTests/FileManagementTests.swift b/ios/engine/KMEI/KeymanEngineTests/FileManagementTests.swift index 50d50123f6..826b16871e 100644 --- a/ios/engine/KMEI/KeymanEngineTests/FileManagementTests.swift +++ b/ios/engine/KMEI/KeymanEngineTests/FileManagementTests.swift @@ -69,4 +69,62 @@ class FileManagementTests: XCTestCase { } } } + + func testInstallKeyboardFromPackage() throws { + ResourceFileManager.shared.prepareKMPInstall(from: TestUtils.Keyboards.khmerAngkorKMP) { kmp, error in + XCTAssertNotNil(kmp, "Failed to prepare KMP for installation") + XCTAssertNil(error, "Error occurred while preparing KMP for installation") + XCTAssertNotNil(kmp as? KeyboardKeymanPackage, "KMP resource type improperly recognized - expected a keyboard package!") + + do { + try ResourceFileManager.shared.install(TestUtils.Keyboards.khmer_angkor, from: kmp!) + } catch { + XCTFail("Unexpected error during KeyboardPackage install") + } + + let installURL = Storage.active.keyboardURL(forID: "khmer_angkor", version: "1.0.6") + + XCTAssertTrue(FileManager.default.fileExists(atPath: installURL.path), + "Could not find installed keyboard file") + + let keyboards = Storage.active.userDefaults.userKeyboards! + + XCTAssertEqual(keyboards.count, 1, "Unexpected number of keyboards were installed") + XCTAssertEqual(keyboards[0].id, "khmer_angkor", "Installed keyboard ID mismatch") + + // While the KMP's version of the specified InstallableKeyboard does specify Fonts, + // the literal-based testing version does NOT. Since we're installing from the predefined, + // test-copy instance, we expect NOT to see the font from this test's install! + // + // Yes, the KeymanPackage.contains check isn't exhaustive - it just does a pair of + // id checks. This version of events couldn't happen if we thorough enough there. + let fontURL = Storage.active.fontURL(forKeyboardID: "khmer_angkor", filename: "Mondulkiri-R.ttf") + XCTAssertFalse(FileManager.default.fileExists(atPath: fontURL.path)) + } + } + + func testInstallLexicalModelFromPackage() { + ResourceFileManager.shared.prepareKMPInstall(from: TestUtils.LexicalModels.mtntKMP) { kmp, error in + XCTAssertNotNil(kmp, "Failed to prepare KMP for installation") + XCTAssertNil(error, "Error occurred while preparing KMP for installation") + XCTAssertNotNil(kmp as? LexicalModelKeymanPackage, "KMP resource type improperly recognized - expected a lexical model package!") + + do { + try ResourceFileManager.shared.install(TestUtils.LexicalModels.mtnt, from: kmp!) + } catch { + XCTFail("Unexpected error during LexicalModelPackage install") + } + + let installURL = Storage.active.lexicalModelURL(forID: "nrc.en.mtnt", version: "0.1.4") + + XCTAssertTrue(FileManager.default.fileExists(atPath: installURL.path), + "Could not find installed lexical model file") + + let models = Storage.active.userDefaults.userLexicalModels! + + // This variant is selective - only a single pairing should be installed for the model. + XCTAssertEqual(models.count, 1, "Unexpected number of models were installed") + XCTAssertEqual(models[0].id, "nrc.en.mtnt", "Installed lexical model ID mismatch") + } + } }