mirror of
https://github.com/keymanapp/keyman.git
synced 2026-09-10 01:27:43 +00:00
feat(ios/engine): package contents auto-update matching installed resources
This commit is contained in:
parent
38771d4a2a
commit
2361fa1871
8 changed files with 104 additions and 5 deletions
|
|
@ -134,6 +134,16 @@ public extension UserDefaults {
|
|||
}
|
||||
}
|
||||
|
||||
func userResources<Resource: LanguageResource>(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 ?? []
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<Resource: LanguageResource,
|
||||
Package: TypedKeymanPackage<Resource>> (
|
||||
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<Resource: LanguageResource,
|
||||
Package: TypedKeymanPackage<Resource>> (
|
||||
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: LanguageResource>(_ resource: Resource) {
|
||||
|
|
|
|||
|
|
@ -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)")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" }))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
Loading…
Add table
Reference in a new issue