diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Extension/UserDefaults+Types.swift b/ios/engine/KMEI/KeymanEngine/Classes/Extension/UserDefaults+Types.swift index 1219cd61a8..7fc5333672 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Extension/UserDefaults+Types.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Extension/UserDefaults+Types.swift @@ -134,6 +134,16 @@ public extension UserDefaults { } } + func userResources(ofType: Resource.Type) -> [Resource]? { + if ofType == InstallableKeyboard.self { + return (userKeyboards as? [Resource]) + } else if ofType == InstallableLexicalModel.self { + return (userLexicalModels as? [Resource]) + } else { + return nil + } + } + var userResources: [AnyLanguageResource]? { get { let keyboards = userKeyboards ?? [] diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Model/LanguageResource.swift b/ios/engine/KMEI/KeymanEngine/Classes/Model/LanguageResource.swift index a1de718954..ddc03e5f9f 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Model/LanguageResource.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Model/LanguageResource.swift @@ -25,6 +25,12 @@ extension LanguageResourceFullID where Self: Equatable { } } +extension LanguageResourceFullID { + var description: String { + return "{\(type): {id = \(id), languageID=\(languageID)}}" + } +} + // Alas, 'associatedtype' stuff isn't exactly generic, and it's impossible to wildcard. // So, this supports Swift's "type erasure" pattern, acting as a "wildcarded" // LanguageResource that doesn't care about the specific associatedtype(s) that diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift index 3ebc30d1dc..26f5cf9982 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift @@ -222,19 +222,52 @@ public class ResourceFileManager { resourceWithID fullID: ResourceType.FullID, from package: PackageType) throws { - guard let resource = package.findResource(withID: fullID) else { + try install(resourcesWithIDs: [fullID], from: package) + } + + public func install> ( + resourcesWithIDs fullIDs: [Resource.FullID], from package: Package) throws { + if fullIDs.contains(where: { package.findResource(withID: $0) == nil }) { + let missingResource = fullIDs.first(where: { package.findResource(withID: $0) == nil })! + log.error("Resource with full ID \(missingResource.description) not in package") throw KMPError.resourceNotInPackage } do { try copyWithOverwrite(from: package.sourceFolder, - to: Storage.active.resourceDir(for: resource)!) + to: Storage.active.packageDir(for: package)!) } catch { log.error("Could not create installation directory and/or copy resources: \(error)") throw KMPError.fileSystem } - addResource(resource) + let updatables = findPotentialUpdates(in: package).map { return $0.typedFullID } + let fullList = fullIDs + updatables + + fullList.forEach { addResource(package.findResource(withID: $0)!) } + } + + internal func findPotentialUpdates> ( + in package: Package, + ignoring resourcesToIgnore: [Resource] = []) -> [Resource] { + let installedResources = Storage.active.userDefaults.userResources(ofType: Resource.self) ?? [] + var updatableResources: [Resource] = [] + + installedResources.forEach { resource in + // If there's no package ID, default to the resource's ID. + // If the package ID matches the resource's package ID and we're not ignoring the resource, + // check to ensure that the package does contain the resource. + if (resource.packageID ?? resource.id) == package.id, + !resourcesToIgnore.contains(where: { $0.typedFullID == resource.typedFullID }) { + if let updatable = package.findResource(withID: resource.typedFullID) { + updatableResources.append(updatable) + } + } + } + + return updatableResources } internal func addResource(_ resource: Resource) { diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/Storage.swift b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/Storage.swift index 9de76a4bfc..d2d612013f 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/Storage.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/Storage.swift @@ -277,7 +277,9 @@ extension Storage { // Perform an auto-install of the lexical model's KMP if not already installed. let defaultKMPFile = defaultLexicalModelDir.appendingPathComponent("\(Defaults.lexicalModel.id).model.kmp") let package = try ResourceFileManager.shared.prepareKMPInstall(from: defaultKMPFile) as! LexicalModelKeymanPackage - try ResourceFileManager.shared.install(resourceWithID: Defaults.lexicalModel.fullID, from: package) + + // Install all languages for the model, not just the default-listed one. + try ResourceFileManager.shared.install(resourcesWithIDs: package.installables[0].map { $0.fullID }, from: package) } catch { log.error("Failed to install the default lexical model from the bundled KMP: \(error)") } diff --git a/ios/engine/KMEI/KeymanEngineTests/FileManagementTests.swift b/ios/engine/KMEI/KeymanEngineTests/FileManagementTests.swift index d01aa32d4a..b1c4312164 100644 --- a/ios/engine/KMEI/KeymanEngineTests/FileManagementTests.swift +++ b/ios/engine/KMEI/KeymanEngineTests/FileManagementTests.swift @@ -118,4 +118,16 @@ class FileManagementTests: XCTestCase { XCTAssertEqual(models.count, 1, "Unexpected number of models were installed") XCTAssertEqual(models[0].id, "nrc.en.mtnt", "Installed lexical model ID mismatch") } + + func testInstallUpdateCheck() throws { + // Has a resource in need of updates (sil_euro_latin) + TestUtils.Migrations.applyBundleToFileSystem(TestUtils.Migrations.cloud_to_kmp_13) + + let package = try ResourceFileManager.shared.prepareKMPInstall(from: TestUtils.Keyboards.silEuroLatinKMP) as! KeyboardKeymanPackage + let updatables = ResourceFileManager.shared.findPotentialUpdates(in: package) + + XCTAssertEqual(updatables.count, 2) + XCTAssertTrue(updatables.contains(where: { $0.languageID == "en" })) + XCTAssertTrue(updatables.contains(where: { $0.languageID == "fr" })) + } } diff --git a/ios/engine/KMEI/KeymanEngineTests/MigrationTests.swift b/ios/engine/KMEI/KeymanEngineTests/MigrationTests.swift index ede1ddf4ff..ae899dcf76 100644 --- a/ios/engine/KMEI/KeymanEngineTests/MigrationTests.swift +++ b/ios/engine/KMEI/KeymanEngineTests/MigrationTests.swift @@ -20,7 +20,30 @@ class MigrationTests: XCTestCase { Migrations.migrate(storage: Storage.active) Migrations.updateResources(storage: Storage.active) - // TODO: test things. + let userDefaults = Storage.active.userDefaults + let userKeyboards = userDefaults.userKeyboards + + // The fun thing to test here - sil_euro_latin is first wrapped with an auto-generated + // kmp.json, THEN updated with the actual KMP. + + let sil_euro_latin_kbds = userKeyboards!.filter { return $0.id == "sil_euro_latin" } + + sil_euro_latin_kbds.forEach { + XCTAssertEqual($0.packageID, "sil_euro_latin") + XCTAssertEqual($0.version, TestUtils.Keyboards.sil_euro_latin.version) + } + + let sil_euro_latin_package = ResourceFileManager.shared.installedPackages.first(where: { $0.id == "sil_euro_latin" }) as! KeyboardKeymanPackage + + XCTAssertFalse(sil_euro_latin_package.metadata.isAutogeneratedWrapper) + + // As it's installing a true package, there will be files existing here that won't exist + // for the others. + let validationFilenames = ["kmp.inf", "readme.htm", "welcome.htm", "sil_euro_latin.kmx", "DejaVuSans.ttf", "currency.png", "usage.htm"] + validationFilenames.forEach { + let file = sil_euro_latin_package.sourceFolder.appendingPathComponent($0) + XCTAssertTrue(FileManager.default.fileExists(atPath: file.path), "Expected file \(file.path) missing") + } } func testVersion13ResourceMigration() { diff --git a/ios/engine/KMEI/KeymanEngineTests/TestUtils/Keyboards.swift b/ios/engine/KMEI/KeymanEngineTests/TestUtils/Keyboards.swift index c8e9e1fdd3..041cd8b7a1 100644 --- a/ios/engine/KMEI/KeymanEngineTests/TestUtils/Keyboards.swift +++ b/ios/engine/KMEI/KeymanEngineTests/TestUtils/Keyboards.swift @@ -12,6 +12,8 @@ import Foundation extension TestUtils { enum Keyboards { static let khmerAngkorKMP = TestUtils.keyboardsBundle.url(forResource: "khmer_angkor", withExtension: "kmp")! + static let silEuroLatinKMP = TestUtils.keyboardsBundle.url(forResource: "sil_euro_latin", withExtension: "kmp")! + static let khmer_angkor = InstallableKeyboard(id: "khmer_angkor", name: "Khmer Angkor", languageID: "km", @@ -21,6 +23,7 @@ extension TestUtils { font: nil, oskFont: nil, isCustom: false) + static let khmer10 = InstallableKeyboard(id: "khmer10", name: "Khmer (NiDA)", languageID: "km", @@ -30,5 +33,15 @@ extension TestUtils { font: nil, oskFont: nil, isCustom: false) + + static let sil_euro_latin = InstallableKeyboard(id: "sil_euro_latin", + name: "EuroLatin (SIL)", + languageID: "en", + languageName: "English", + version: "1.9.1", + isRTL: false, + font: Font(family: "LatinWeb", source: ["DejaVuSans.ttf"], size: nil), + oskFont: nil, + isCustom: false) } } diff --git a/ios/engine/KMEI/KeymanEngineTests/resources/Keyboards.bundle/sil_euro_latin.kmp b/ios/engine/KMEI/KeymanEngineTests/resources/Keyboards.bundle/sil_euro_latin.kmp new file mode 100644 index 0000000000..9b999bd2ef Binary files /dev/null and b/ios/engine/KMEI/KeymanEngineTests/resources/Keyboards.bundle/sil_euro_latin.kmp differ