diff --git a/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj b/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj index 44f8c95fe2..f1e0ea0ae4 100644 --- a/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj +++ b/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj @@ -67,6 +67,7 @@ C082CE151F90AFD400860F02 /* Collection+SafeAccess.swift in Sources */ = {isa = PBXBuildFile; fileRef = C082CE141F90AFD400860F02 /* Collection+SafeAccess.swift */; }; C0959CD41F99C44E00B616BC /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0959CD31F99C44E00B616BC /* Constants.swift */; }; C0A93A541F8B21240079948B /* Manager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A93A531F8B21240079948B /* Manager.swift */; }; + C0B901AA1FA1AFC200764EB8 /* UserDefaults+Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0B901A91FA1AFC200764EB8 /* UserDefaults+Types.swift */; }; C0CE581E1F877B4D00AE6A7E /* NSNotification.Name+Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0CE581D1F877B4D00AE6A7E /* NSNotification.Name+Notifications.swift */; }; C0D3F35E1F9F33490055C7CF /* Options.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D3F35D1F9F33490055C7CF /* Options.swift */; }; C0D3F3601F9F3AD80055C7CF /* InstallableKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D3F35F1F9F3AD80055C7CF /* InstallableKeyboard.swift */; }; @@ -184,6 +185,7 @@ C0959CD31F99C44E00B616BC /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; C0A5FF361F6682EB00BE740C /* PopoverView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PopoverView.swift; sourceTree = ""; }; C0A93A531F8B21240079948B /* Manager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Manager.swift; sourceTree = ""; }; + C0B901A91FA1AFC200764EB8 /* UserDefaults+Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+Types.swift"; sourceTree = ""; }; C0CE581D1F877B4D00AE6A7E /* NSNotification.Name+Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSNotification.Name+Notifications.swift"; sourceTree = ""; }; C0D3F35D1F9F33490055C7CF /* Options.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Options.swift; sourceTree = ""; }; C0D3F35F1F9F3AD80055C7CF /* InstallableKeyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallableKeyboard.swift; sourceTree = ""; }; @@ -385,6 +387,7 @@ C075EB051F8EFF870041F4BD /* String+Helpers.swift */, C082CE141F90AFD400860F02 /* Collection+SafeAccess.swift */, C06085B31F9485E40057E5B9 /* UIButton+Helpers.swift */, + C0B901A91FA1AFC200764EB8 /* UserDefaults+Types.swift */, ); path = Extension; sourceTree = ""; @@ -752,6 +755,7 @@ C0CE581E1F877B4D00AE6A7E /* NSNotification.Name+Notifications.swift in Sources */, C0A93A541F8B21240079948B /* Manager.swift in Sources */, C06D373E1F81F5C400F61AE0 /* KeyboardInfoViewController.swift in Sources */, + C0B901AA1FA1AFC200764EB8 /* UserDefaults+Types.swift in Sources */, C06D373F1F81F5C400F61AE0 /* KeyboardPickerViewController.swift in Sources */, C0324B911F8763E100AF3785 /* TextViewDelegateProxy.swift in Sources */, C040E50E1F85FF8A00901EE4 /* KeyPreviewView.swift in Sources */, diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Constants.swift b/ios/engine/KMEI/KeymanEngine/Classes/Constants.swift index ac5985eb32..d22d46fe2b 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Constants.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Constants.swift @@ -66,6 +66,7 @@ public struct Key { static let update = "update" } +// TODO: Remove public struct DefaultKeyboard { public static let keyboardID = "european2" public static let languageID = "eng" @@ -84,3 +85,16 @@ public struct DefaultKeyboard { Key.font: keyboardFont ] } + +public struct Constants { + private static let defaultFont = Font(family: "LatinWeb", source: ["DejaVuSans.ttf"], size: nil) + public static let defaultKeyboard = InstallableKeyboard(id: "european2", + name: "EuroLatin2 Keyboard", + languageID: "eng", + languageName: "English", + version: "1.6", + isRTL: false, + font: defaultFont, + oskFont: nil, + isCustom: false) +} diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Extension/UserDefaults+Types.swift b/ios/engine/KMEI/KeymanEngine/Classes/Extension/UserDefaults+Types.swift new file mode 100644 index 0000000000..ff6b9a24ab --- /dev/null +++ b/ios/engine/KMEI/KeymanEngine/Classes/Extension/UserDefaults+Types.swift @@ -0,0 +1,55 @@ +// +// UserDefaults+Types.swift +// KeymanEngine +// +// Created by Gabriel Wong on 2017-10-26. +// Copyright © 2017 SIL International. All rights reserved. +// + +import Foundation + +public extension UserDefaults { + public func installableKeyboards(forKey key: String) -> [InstallableKeyboard]? { + guard let array = array(forKey: key) as? [Data] else { + return nil + } + let decoder = PropertyListDecoder() + do { + return try array.map { try decoder.decode(InstallableKeyboard.self, from: $0) } + } catch { + Manager.shared.kmLog("UserDefaults: Error decoding keyboards: \(error)", checkDebugPrinting: false) + return nil + } + } + + public func set(_ keyboards: [InstallableKeyboard], forKey key: String) { + let encoder = PropertyListEncoder() + do { + let array = try keyboards.map { try encoder.encode($0) } + set(array, forKey: key) + } catch { + Manager.shared.kmLog("UserDefaults: Error encoding keyboards: \(error)", checkDebugPrinting: false) + } + } + + public func installableKeyboard(forKey key: String) -> InstallableKeyboard? { + if let data = self.data(forKey: key) { + do { + return try PropertyListDecoder().decode(InstallableKeyboard.self, from: data) + } catch { + Manager.shared.kmLog("UserDefaults: Error decoding keyboard: \(error)", checkDebugPrinting: false) + return nil + } + } + return nil + } + + public func set(_ keyboard: InstallableKeyboard, forKey key: String) { + do { + let data = try PropertyListEncoder().encode(keyboard) + set(data, forKey: key) + } catch { + Manager.shared.kmLog("UserDefaults: Error encoding keyboard: \(error)", checkDebugPrinting: false) + } + } +} diff --git a/ios/engine/KMEI/KeymanEngine/Classes/InputViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/InputViewController.swift index 02b5a54f36..7de575606f 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/InputViewController.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/InputViewController.swift @@ -54,7 +54,7 @@ open class InputViewController: UIInputViewController, KeymanWebViewDelegate { private var keyboardListCount: Int { let userData = Manager.shared.activeUserDefaults() - let keyboardList = userData.array(forKey: Key.userKeyboardsList) + let keyboardList = userData.installableKeyboards(forKey: Key.userKeyboardsList) return keyboardList?.count ?? 0 } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/KeyboardMenuView.swift b/ios/engine/KMEI/KeymanEngine/Classes/KeyboardMenuView.swift index 9ea78861c6..7f7f731d8e 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/KeyboardMenuView.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/KeyboardMenuView.swift @@ -36,7 +36,7 @@ class KeyboardMenuView: UIView, UITableViewDelegate, UITableViewDataSource, UIGe } let userData = Manager.shared.activeUserDefaults() - let keyboardList = userData.array(forKey: Key.userKeyboardsList) + let keyboardList = userData.installableKeyboards(forKey: Key.userKeyboardsList) let titleCloseButton: String if let closeButtonTitle = closeButtonTitle { @@ -290,13 +290,10 @@ class KeyboardMenuView: UIView, UITableViewDelegate, UITableViewDataSource, UIGe cell.isSelected = false cell.accessoryType = .disclosureIndicator } else { - let keyboard = tableList[indexPath.row] as? [AnyHashable: Any] - let languageID = keyboard?[Key.languageId] as? String - let keyboardID = keyboard?[Key.keyboardId] as? String - cell.textLabel?.text = keyboard?[Key.keyboardName] as? String + let keyboard = tableList[indexPath.row] as! InstallableKeyboard + cell.textLabel?.text = keyboard.name cell.tag = indexPath.row - if (Manager.shared.languageID == languageID) && - (Manager.shared.keyboardID == keyboardID) { + if (Manager.shared.languageID == keyboard.languageID) && (Manager.shared.keyboardID == keyboard.id) { cell.selectionStyle = .none cell.isSelected = true cell.accessoryType = .checkmark @@ -338,18 +335,10 @@ class KeyboardMenuView: UIView, UITableViewDelegate, UITableViewDataSource, UIGe inputViewController?.advanceToNextInputMode() return } - guard let kb = tableList[index] as? [AnyHashable: Any] else { + guard let kb = tableList[index] as? InstallableKeyboard else { return } - let langID = kb[Key.languageId] as? String - let kbID = kb[Key.keyboardId] as? String - let langName = kb[Key.languageName] as? String - let kbName = kb[Key.keyboardName] as? String - let font = kb[Key.font] as? String - let oskFont = kb[Key.oskFont] as? String - - if Manager.shared.setKeyboard(withID: kbID!, languageID: langID!, keyboardName: kbName, - languageName: langName, font: font, oskFont: oskFont) { + if Manager.shared.setKeyboard(kb) { tableView?.reloadData() } } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/KeyboardInfoViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/KeyboardInfoViewController.swift index c8756107af..dfab331b09 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/KeyboardInfoViewController.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/KeyboardInfoViewController.swift @@ -155,24 +155,16 @@ class KeyboardInfoViewController: UITableViewController, UIAlertViewDelegate { if alertView.tag == 1 { let userData = Manager.shared.activeUserDefaults() - let userKeyboards = userData.array(forKey: Key.userKeyboardsList) as! [[String: String]] - let kbDict = userKeyboards[keyboardIndex] + let userKeyboards = userData.installableKeyboards(forKey: Key.userKeyboardsList)! + let kb = userKeyboards[keyboardIndex] if Manager.shared.removeKeyboard(at: keyboardIndex) { if isCurrentKeyboard { // Select default keyboard - - let kbID = userKeyboards[0][Key.keyboardId]! - let langID = userKeyboards[0][Key.languageId]! - let kbName = userKeyboards[0][Key.keyboardName] - let langName = userKeyboards[0][Key.languageName] - let font = userKeyboards[0][Key.font] - let oskFont = userKeyboards[0][Key.oskFont] - Manager.shared.setKeyboard(withID: kbID, languageID: langID, keyboardName: kbName, - languageName: langName, font: font, oskFont: oskFont) + Manager.shared.setKeyboard(kb) } NotificationCenter.default.post(name: NSNotification.Name.keymanKeyboardRemoved, - object: self, userInfo: [Key.keyboardInfo: kbDict] + object: self, userInfo: [Key.keyboardInfo: kb] ) navigationController?.popToRootViewController(animated: true) } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/KeyboardPickerViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/KeyboardPickerViewController.swift index 845542f5b3..48d66e1e69 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/KeyboardPickerViewController.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/KeyboardPickerViewController.swift @@ -14,8 +14,8 @@ private let toolbarLabelTag = 101 private let toolbarActivityIndicatorTag = 102 class KeyboardPickerViewController: UITableViewController, UIAlertViewDelegate { - private var userKeyboards: [[String: String]] = [] - private var updateQueue: [[String: String]]? + private var userKeyboards: [InstallableKeyboard] = [] + private var updateQueue: [InstallableKeyboard]? private var _isDoneButtonEnabled = false private var isDidUpdateCheck = false @@ -131,26 +131,15 @@ class KeyboardPickerViewController: UITableViewController, UIAlertViewDelegate { return } - let kbDict = userKeyboards[indexPath.row] - let selKbID = kbDict[Key.keyboardId] - let selLangID = kbDict[Key.languageId] - let isCurrentKB = isCurrentKeyboard(languageID: selLangID, keyboardID: selKbID) - if Manager.shared.removeKeyboard(at: indexPath.row) { - let userData = Manager.shared.activeUserDefaults() - userKeyboards = userData.array(forKey: Key.userKeyboardsList) as! [[String : String]] - if isCurrentKB { - let kbID = userKeyboards[0][Key.keyboardId] - let langID = userKeyboards[0][Key.languageId] - let kbName = userKeyboards[0][Key.keyboardName] - let langName = userKeyboards[0][Key.languageName] - let font = userKeyboards[0][Key.font] - let oskFont = userKeyboards[0][Key.oskFont] - Manager.shared.setKeyboard(withID: kbID!, languageID: langID!, keyboardName: kbName, - languageName: langName, font: font, oskFont: oskFont) + let kb = userKeyboards[indexPath.row] + if isCurrentKeyboard(languageID: kb.id, keyboardID: kb.languageID) { + let userData = Manager.shared.activeUserDefaults() + userKeyboards = userData.installableKeyboards(forKey: Key.userKeyboardsList)! + Manager.shared.setKeyboard(userKeyboards[0]) } NotificationCenter.default.post(name: NSNotification.Name.keymanKeyboardRemoved, - object: self, userInfo: [Key.keyboardInfo: kbDict]) + object: self, userInfo: [Key.keyboardInfo: kb]) self.tableView.reloadData() } setIsDoneButtonEnabled(true) @@ -163,22 +152,17 @@ class KeyboardPickerViewController: UITableViewController, UIAlertViewDelegate { func showKeyboardInfoView(with index: Int) { setIsDoneButtonEnabled(true) - let kbID = userKeyboards[index][Key.keyboardId]! - let langID = userKeyboards[index][Key.languageId]! - let kbName = userKeyboards[index][Key.keyboardName] - let isCustom = userKeyboards[index][Key.customKeyboard] == "Y" - let kbVersion = userKeyboards[index][Key.keyboardVersion] - ?? Manager.shared.latestKeyboardFileVersion(withID: kbID) - ?? "1.0" + let kb = userKeyboards[index] + let version = kb.version let infoView = KeyboardInfoViewController() - infoView.title = kbName + infoView.title = kb.name infoView.keyboardCount = userKeyboards.count infoView.keyboardIndex = index - infoView.keyboardID = kbID - infoView.languageID = langID - infoView.keyboardVersion = kbVersion - infoView.isCustomKeyboard = isCustom + infoView.keyboardID = kb.id + infoView.languageID = kb.languageID + infoView.keyboardVersion = version + infoView.isCustomKeyboard = kb.isCustom navigationController?.pushViewController(infoView, animated: true) } @@ -186,14 +170,13 @@ class KeyboardPickerViewController: UITableViewController, UIAlertViewDelegate { willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { cell.selectionStyle = .none - let languageID = userKeyboards[indexPath.row][Key.languageId] - let keyboardID = userKeyboards[indexPath.row][Key.keyboardId] + let kb = userKeyboards[indexPath.row] - cell.textLabel?.text = userKeyboards[indexPath.row][Key.languageName] - cell.detailTextLabel?.text = userKeyboards[indexPath.row][Key.keyboardName] + cell.textLabel?.text = kb.languageName + cell.detailTextLabel?.text = kb.name cell.tag = indexPath.row - if isCurrentKeyboard(languageID: languageID, keyboardID: keyboardID) { + if isCurrentKeyboard(languageID: kb.languageID, keyboardID: kb.id) { cell.selectionStyle = .blue cell.isSelected = true cell.accessoryType = .detailDisclosureButton @@ -222,16 +205,17 @@ class KeyboardPickerViewController: UITableViewController, UIAlertViewDelegate { Manager.shared.shouldReloadKeyboard = true // Update keyboard version - let downloadedKbInfo = notification.userInfo?[Key.keyboardInfo] as? [String: String] - if let kbID = downloadedKbInfo?[Key.keyboardId], - let currentKbInfo = Manager.shared.keyboardsInfo?[kbID] { - Manager.shared.updateKeyboardVersion(forID: kbID, newKeyboardVersion: currentKbInfo.version) + let keyboards = notification.userInfo?[Key.keyboardInfo] as! [InstallableKeyboard] + for keyboard in keyboards { + if let currentKbInfo = Manager.shared.keyboardsInfo?[keyboard.id] { + Manager.shared.updateKeyboardVersion(forID: keyboard.id, newKeyboardVersion: currentKbInfo.version) + } } updateQueue!.remove(at: 0) if !updateQueue!.isEmpty { - let langID = updateQueue![0][Key.languageId]! - let kbID = updateQueue![0][Key.keyboardId]! + let langID = updateQueue![0].languageID + let kbID = updateQueue![0].id Manager.shared.downloadKeyboard(withID: kbID, languageID: langID, isUpdate: true) } else { loadUserKeyboards() @@ -260,23 +244,13 @@ class KeyboardPickerViewController: UITableViewController, UIAlertViewDelegate { navigationItem.rightBarButtonItem?.isEnabled = true } - let kbInfo = notification.userInfo?[Key.keyboardInfo] as? [String: String] - let langID = kbInfo![Key.languageId]! - let kbID = kbInfo![Key.keyboardId]! - let key = "\(langID)_\(kbID)" - let keyboardDict = Manager.shared.keyboardsDictionary[key] - let langName = keyboardDict![Key.languageName]! - let kbName = keyboardDict![Key.keyboardName]! - let isRTL = keyboardDict?[Key.keyboardRTL] == "Y" - let font = keyboardDict?[Key.font] - let oskFont = keyboardDict?[Key.oskFont] - // Add keyboard. - Manager.shared.addKeyboard(withID: kbID, languageID: langID, keyboardName: kbName, - languageName: langName, isRTL: isRTL, isCustom: false, font: font, - oskFont: oskFont) - Manager.shared.setKeyboard(withID: kbID, languageID: langID, keyboardName: kbName, - languageName: langName, font: font, oskFont: oskFont) + let keyboards = notification.userInfo![Key.keyboardInfo] as! [InstallableKeyboard] + for keyboard in keyboards { + Manager.shared.addKeyboard(keyboard) + Manager.shared.setKeyboard(keyboard) + } + navigationController?.popToRootViewController(animated: true) } } @@ -309,15 +283,7 @@ class KeyboardPickerViewController: UITableViewController, UIAlertViewDelegate { private func switchKeyboard(_ index: Int) { // Switch keyboard and register to user defaults. - let langID = userKeyboards[index][Key.languageId]! - let kbID = userKeyboards[index][Key.keyboardId]! - let langName = userKeyboards[index][Key.languageName] - let kbName = userKeyboards[index][Key.keyboardName] - let font = userKeyboards[index][Key.font] - let oskFont = userKeyboards[index][Key.oskFont] - - if Manager.shared.setKeyboard(withID: kbID, languageID: langID, keyboardName: kbName, - languageName: langName, font: font, oskFont: oskFont) { + if Manager.shared.setKeyboard(userKeyboards[index]) { tableView.reloadData() } @@ -329,20 +295,10 @@ class KeyboardPickerViewController: UITableViewController, UIAlertViewDelegate { private func loadUserKeyboards() { let userData = Manager.shared.activeUserDefaults() - if let userKeyboards = userData.array(forKey: Key.userKeyboardsList) as? [[String: String]] { + if let userKeyboards = userData.installableKeyboards(forKey: Key.userKeyboardsList) { self.userKeyboards = userKeyboards } else { - let kbVersion = Manager.shared.latestKeyboardFileVersion(withID: DefaultKeyboard.keyboardID) - userKeyboards = [[ - Key.keyboardId: DefaultKeyboard.keyboardID, - Key.languageId: DefaultKeyboard.languageID, - Key.keyboardName: DefaultKeyboard.keyboardName, - Key.languageName: DefaultKeyboard.languageName, - Key.keyboardVersion: kbVersion ?? "nil", - Key.keyboardRTL: DefaultKeyboard.keyboardRTL, - Key.font: DefaultKeyboard.keyboardFont - ]] - + userKeyboards = [Constants.defaultKeyboard] userData.set(userKeyboards, forKey: Key.userKeyboardsList) userData.synchronize() } @@ -351,11 +307,7 @@ class KeyboardPickerViewController: UITableViewController, UIAlertViewDelegate { } private func isAdded(languageID langID: String, keyboardID kbID: String) -> Bool { - return userKeyboards.contains { keyboard in - let languageID = keyboard[Key.languageId] - let keyboardID = keyboard[Key.keyboardId] - return languageID == langID && keyboardID == kbID - } + return userKeyboards.contains { kb in kb.languageID == langID && kb.id == kbID } } @objc func doneClicked(_ sender: Any) { @@ -406,39 +358,34 @@ class KeyboardPickerViewController: UITableViewController, UIAlertViewDelegate { isDidUpdateCheck = true return userKeyboards.contains { keyboard in - let kbID = keyboard[Key.keyboardId]! + let kbID = keyboard.id return Manager.shared.stateForKeyboard(withID: kbID) == .needsUpdate } } private func updateKeyboards() { - updateQueue = [[String: String]]() + updateQueue = [] var kbIDs = Set() for kb in userKeyboards { - guard let kbID = kb[Key.keyboardId] else { - continue - } - let kbState = Manager.shared.stateForKeyboard(withID: kbID) + let kbState = Manager.shared.stateForKeyboard(withID: kb.id) if kbState == .needsUpdate { - if !kbIDs.contains(kbID) { - kbIDs.insert(kbID) + if !kbIDs.contains(kb.id) { + kbIDs.insert(kb.id) updateQueue!.append(kb) } } } if !updateQueue!.isEmpty { - let langID = updateQueue![0][Key.languageId]! - let kbID = updateQueue![0][Key.keyboardId]! + let langID = updateQueue![0].languageID + let kbID = updateQueue![0].id Manager.shared.downloadKeyboard(withID: kbID, languageID: langID, isUpdate: true) } } private func scroll(toSelectedKeyboard animated: Bool) { let index = userKeyboards.index { kb in - let langID = kb[Key.languageId] - let kbID = kb[Key.keyboardId] - return isCurrentKeyboard(languageID: langID, keyboardID: kbID) + return isCurrentKeyboard(languageID: kb.languageID, keyboardID: kb.id) } if let index = index { diff --git a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LanguageDetailViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LanguageDetailViewController.swift index d0209f71c4..c934fba25e 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LanguageDetailViewController.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LanguageDetailViewController.swift @@ -13,7 +13,7 @@ private let toolbarLabelTag = 101 private let toolbarActivityIndicatorTag = 102 class LanguageDetailViewController: UITableViewController, UIAlertViewDelegate { - private var userKeyboards: [String: [String: Any]] = [:] + private var userKeyboards: [String: InstallableKeyboard] = [:] private var isUpdate = false private let language: Language @@ -154,17 +154,14 @@ class LanguageDetailViewController: UITableViewController, UIAlertViewDelegate { private func loadUserKeyboards() { let userData = Manager.shared.activeUserDefaults() - guard let userKbList = userData.array(forKey: Key.userKeyboardsList) as? [[String: String]], - !userKbList.isEmpty else { + guard let userKbList = userData.installableKeyboards(forKey: Key.userKeyboardsList), !userKbList.isEmpty else { userKeyboards = [:] return } userKeyboards = [:] for kb in userKbList { - let langID = kb[Key.languageId] - let kbID = kb[Key.keyboardId] - let dictKey = "\(langID!)_\(kbID!)" + let dictKey = "\(kb.languageID)_\(kb.id)" userKeyboards[dictKey] = kb } } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LanguageViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LanguageViewController.swift index 74cfbf63dc..533633f591 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LanguageViewController.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LanguageViewController.swift @@ -275,16 +275,14 @@ class LanguageViewController: UITableViewController, UIAlertViewDelegate { func loadUserKeyboards() { userKeyboards = [:] let userData = Manager.shared.activeUserDefaults() - guard let userKbList = userData.array(forKey: Key.userKeyboardsList) as? [[String: Any]] else { + guard let userKbList = userData.installableKeyboards(forKey: Key.userKeyboardsList) else { userKeyboards = [:] return } for kb in userKbList { - if let langID = kb[Key.languageId] as? String, let kbID = kb[Key.keyboardId] as? String { - let dictKey = "\(langID)_\(kbID)" - userKeyboards[dictKey] = kb - } + let dictKey = "\(kb.languageID)_\(kb.id)" + userKeyboards[dictKey] = kb } } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift b/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift index d34837d37a..cefc6ac1fe 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift @@ -105,7 +105,7 @@ UIGestureRecognizerDelegate { /// Dictionary of Keyman keyboards to store language and keyboard names etc. /// /// The key format is $languageID_$keyboardID. For example, "eng_european2" returns the English EuroLatin2 keyboard - public private(set) var keyboardsDictionary: [String: [String: String]] = [:] + public private(set) var keyboardsDictionary: [String: InstallableKeyboard] = [:] /// Dictionary of available Keyman keyboard fonts public private(set) var keymanFonts: [String: [String: Any]] = [:] @@ -234,341 +234,72 @@ UIGestureRecognizerDelegate { // MARK: - Keyboard management - // TODO: Lots of duplicated logic in the setKeyboard() functions - - /// Set the current keyboard. + /// Sets the current keyboard, querying from the user's list of keyboards. /// /// - Precondition: - /// - The language/keyboard list must be loaded (see `fetchKeyboardsList()`) - /// - The keyboard must be downloaded or preloaded. + /// - The keyboard must be added with `addKeyboard()`. /// /// - SeeAlso: - /// fetchKeyboardsList() - /// downloadKeyboard(withID:languageID:isUpdate:) + /// - addKeyboard() /// - Returns: Whether the keyboard was set successfully public func setKeyboard(withID keyboardID: String, languageID: String) -> Bool { - if languageID == self.languageID && keyboardID == self.keyboardID { - return false + if let keyboard = userKeyboard(withID: keyboardID, languageID: languageID) { + return setKeyboard(keyboard) } - - kmLog("Setting language: \(languageID)_\(keyboardID)", checkDebugPrinting: true) - if usingTempFolder { - copyKeymanFilesToTemp() - } - - if keyboardID.isEmpty || languageID.isEmpty { - return false - } - - let kbState = stateForKeyboard(withID: keyboardID) - if kbState == .needsDownload { - kmLog("Could not set keyboardID to \(keyboardID) because the keyboard file does not exist", - checkDebugPrinting: false) - if (self.keyboardID == nil || self.languageID == nil) && keyboardID != DefaultKeyboard.keyboardID { - setKeyboard(withID: DefaultKeyboard.keyboardID, languageID: DefaultKeyboard.languageID, - keyboardName: DefaultKeyboard.keyboardName, languageName: DefaultKeyboard.languageName, - font: DefaultKeyboard.keyboardFont, oskFont: nil) - } - return false - } - - self.languageID = languageID - self.keyboardID = keyboardID - let key = "\(languageID)_\(keyboardID)" - - let userKeyboards = activeUserDefaults().array(forKey: Key.userKeyboardsList) - let index = indexForUserKeyboard(withID: keyboardID, languageID: languageID) - - let kbDict: [String: String]? - if let index = index { - kbDict = userKeyboards![index] as? [String: String] - } else { - kbDict = keyboardsDictionary[key] - } - - var langName = kbDict?[Key.languageName] - var kbName = kbDict?[Key.keyboardName] - let kbVersion = latestKeyboardFileVersion(withID: keyboardID) ?? "1.0" - let isRTL = kbDict?[Key.keyboardRTL] - - if key == "\(DefaultKeyboard.languageID)_\(DefaultKeyboard.keyboardID)" { - if langName == nil || kbName == nil { - langName = DefaultKeyboard.languageName - kbName = DefaultKeyboard.keyboardName - } - } - - var jsFont = self.jsFont(forKeyboardID: keyboardID, languageID: languageID) - var jsOskFont = self.jsOskFont(forKeyboardID: keyboardID, languageID: languageID) - if jsOskFont == "''" { - jsOskFont = jsFont - } - let escapedLangName = langName!.replacingOccurrences(of: "'", with: "\\'") - let escapedKbName = kbName!.replacingOccurrences(of: "'", with: "\\'") - if jsFont == "''" { - jsFont = "undefined" - } - if jsOskFont == "''" { - jsOskFont = "undefined" - } - let jsString = "setKeymanLanguage('\(escapedKbName)','\(keyboardID)','\(escapedLangName)'," + - "'\(languageID)','\(kbVersion)',\(jsFont),\(jsOskFont))" - kmLog("Evaluating JavaScript: \(jsString)", checkDebugPrinting: true) - inputView.evaluateJavaScript(jsString, completionHandler: nil) - - let userData = isSystemKeyboard ? UserDefaults.standard : activeUserDefaults() - - userData.set([ - Key.keyboardId: keyboardID, - Key.languageId: languageID, - Key.keyboardName: kbName, - Key.languageName: langName, - Key.keyboardVersion: kbVersion, - Key.keyboardRTL: isRTL ?? "N", - Key.font: jsFont - ], forKey: Key.userCurrentKeyboard) - userData.synchronize() - - if isKeymanHelpOn { - helpBubbleView?.removeFromSuperview() - let showHelpBubble = #selector(self.showHelpBubble as () -> Void) - NSObject.cancelPreviousPerformRequests(withTarget: self, selector: showHelpBubble, object: nil) - perform(showHelpBubble, with: nil, afterDelay: 1.5) - } - - let kbInfo = [ - Key.keyboardId: keyboardID, - Key.languageId: languageID, - Key.keyboardName: escapedKbName, - Key.languageName: escapedLangName, - Key.keyboardVersion: kbVersion, - Key.font: jsFont - ] - - NotificationCenter.default.post(name: .keymanKeyboardChanged, object: self, - userInfo: [Key.keyboardInfo: kbInfo]) - return true + return false } /// Set the current keyboard. /// - /// - Precondition: - /// - The language/keyboard list must be loaded (see `fetchKeyboardsList()`) - /// - The keyboard must be downloaded or preloaded. - /// - /// - Parameters: - /// - keyboardName: Keyboard name for display on spacebar - /// - languageName: Language name for display on spacebar - /// - /// - SeeAlso: - /// fetchKeyboardsList() - /// downloadKeyboard(withID:languageID:isUpdate:) /// - Returns: Whether the keyboard was set successfully - func setKeyboard(withID keyboardID: String, languageID: String, - keyboardName: String?, languageName: String?) -> Bool { - if keyboardName == nil || languageName == nil { - return setKeyboard(withID: keyboardID, languageID: languageID) - } - - if languageID == self.languageID && keyboardID == self.keyboardID { + public func setKeyboard(_ kb: InstallableKeyboard) -> Bool { + if kb.languageID == self.languageID && kb.id == self.keyboardID { + kmLog("Keyboard unchanged: \(kb.languageID)_\(kb.id)", checkDebugPrinting: true) return false } - kmLog("Setting language: \(languageID)_\(keyboardID)", checkDebugPrinting: true) - if usingTempFolder { - copyKeymanFilesToTemp() - } - - if keyboardID.isEmpty || languageID.isEmpty { - return false - } - - let kbState = stateForKeyboard(withID: keyboardID) - if kbState == .needsDownload { - kmLog("Could not set keyboardID to \(keyboardID) because the keyboard file does not exist", - checkDebugPrinting: false) - if (self.keyboardID == nil || self.languageID == nil) && keyboardID != DefaultKeyboard.keyboardID { - setKeyboard(withID: DefaultKeyboard.keyboardID, languageID: DefaultKeyboard.languageID, - keyboardName: DefaultKeyboard.keyboardName, languageName: DefaultKeyboard.languageName, - font: DefaultKeyboard.keyboardFont, oskFont: nil) - } - return false - } - - self.languageID = languageID - self.keyboardID = keyboardID - let key = "\(languageID)_\(keyboardID)" - - let userKeyboards = activeUserDefaults().array(forKey: Key.userKeyboardsList) - let index = indexForUserKeyboard(withID: keyboardID, languageID: languageID) - - let kbDict: [String: String]? - if let index = index { - kbDict = userKeyboards![index] as? [String: String] - } else { - kbDict = keyboardsDictionary[key] - } - - let kbVersion = latestKeyboardFileVersion(withID: keyboardID) ?? "1.0" - let isRTL = kbDict?[Key.keyboardRTL] - - var jsFont = self.jsFont(forKeyboardID: keyboardID, languageID: languageID) - var jsOskFont = self.jsOskFont(forKeyboardID: keyboardID, languageID: languageID) - if jsOskFont == "''" { - jsOskFont = jsFont - } - let escapedLangName = languageName!.replacingOccurrences(of: "'", with: "\\'") - let escapedKbName = keyboardName!.replacingOccurrences(of: "'", with: "\\'") - if jsFont == "''" { - jsFont = "undefined" - } - if jsOskFont == "''" { - jsOskFont = "undefined" - } - let jsString = "setKeymanLanguage('\(escapedKbName)','\(keyboardID)','\(escapedLangName)'," + - "'\(languageID)','\(kbVersion)',\(jsFont),\(jsOskFont))" - kmLog("Evaluating JavaScript: \(jsString)", checkDebugPrinting: true) - inputView.evaluateJavaScript(jsString, completionHandler: nil) - - let userData = isSystemKeyboard ? UserDefaults.standard : activeUserDefaults() - - userData.set([ - Key.keyboardId: keyboardID, - Key.languageId: languageID, - Key.keyboardName: keyboardName, - Key.languageName: languageName, - Key.keyboardVersion: kbVersion, - Key.keyboardRTL: isRTL ?? "N", - Key.font: jsFont - ], forKey: Key.userCurrentKeyboard) - userData.synchronize() - - if isKeymanHelpOn { - helpBubbleView?.removeFromSuperview() - let showHelpBubble = #selector(self.showHelpBubble as () -> Void) - NSObject.cancelPreviousPerformRequests(withTarget: self, selector: showHelpBubble, object: nil) - perform(showHelpBubble, with: nil, afterDelay: 1.5) - } - - let kbInfo = [ - Key.keyboardId: keyboardID, - Key.languageId: languageID, - Key.keyboardName: escapedKbName, - Key.languageName: escapedLangName, - Key.keyboardVersion: kbVersion, - Key.font: jsFont - ] - - NotificationCenter.default.post(name: .keymanKeyboardChanged, object: self, - userInfo: [Key.keyboardInfo: kbInfo]) - return true - - } - - /// Set the current keyboard. - /// - /// - Precondition: - /// - The language/keyboard list must be loaded (see fetchKeyboardsList()) - /// - The keyboard must be downloaded or preloaded. - /// - /// - Parameters: - /// - keyboardName: Keyboard name for display on spacebar - /// - languageName: Language name for display on spacebar - /// - font: JSON font object or file name of preloaded font in the text field - /// - oskFont: JSON font object or file name of preloaded font for the on-screen keyboard - /// - /// - SeeAlso: - /// fetchKeyboardsList() - /// downloadKeyboard(withID:languageID:isUpdate:) - /// preloadLanguageFile(atPath:shouldOverwrite:) - /// keyboardDictionary - /// preloadFontFile(atPath:shouldOverwrite:) - /// - Returns: Whether the keyboard was set successfully - public func setKeyboard(withID keyboardID: String, languageID: String, - keyboardName: String?, languageName: String?, - font: String?, oskFont: String?) -> Bool { - // if the font and the oskFont are both nil call setKeyboardWithID without these parameters - if font == nil && oskFont == nil { - return setKeyboard(withID: keyboardID, languageID: languageID, - keyboardName: keyboardName, languageName: languageName) - } - - if languageID == self.languageID && keyboardID == self.keyboardID { - return false - } - - kmLog("Setting language: \(languageID)_\(keyboardID)", checkDebugPrinting: true) + kmLog("Setting language: \(kb.languageID)_\(kb.id)", checkDebugPrinting: true) if usingTempFolder { if !copyKeymanFilesToTemp() { return false } } - if keyboardID.isEmpty || languageID.isEmpty { - return false - } - - let kbState = stateForKeyboard(withID: keyboardID) - if kbState == .needsDownload { - kmLog("Could not set keyboardID to \(keyboardID) because the keyboard file does not exist", + // FIXME: kb.version is not respected. Ideally we should be able to trust that the version number in UserDefaults + // is-to-date but it is sometimes not updated. + guard let kbVersion = latestKeyboardFileVersion(withID: kb.id) else { + kmLog("Could not set keyboardID to \(kb.id) because the keyboard file does not exist", checkDebugPrinting: false) - if (self.keyboardID == nil || self.languageID == nil) && keyboardID != DefaultKeyboard.keyboardID { - _ = setKeyboard(withID: DefaultKeyboard.keyboardID, languageID: DefaultKeyboard.languageID, - keyboardName: DefaultKeyboard.keyboardName, languageName: DefaultKeyboard.languageName, - font: DefaultKeyboard.keyboardFont, oskFont: nil) + // Fallback to default keyboard if no keyboard is currently set. + if (self.keyboardID == nil || self.languageID == nil) && kb.id != Constants.defaultKeyboard.id { + _ = setKeyboard(Constants.defaultKeyboard) } return false } - self.languageID = languageID - self.keyboardID = keyboardID - let key = "\(languageID)_\(keyboardID)" + self.languageID = kb.languageID + self.keyboardID = kb.id - let userKeyboards = activeUserDefaults().array(forKey: Key.userKeyboardsList) - let index = indexForUserKeyboard(withID: keyboardID, languageID: languageID) - - let kbDict: [String: String]? - if let index = index { - kbDict = userKeyboards![index] as? [String: String] + let jsFont = self.jsFont(fromFont: kb.font) ?? "undefined" + let jsOskFont: String + if let oskFont = kb.oskFont { + jsOskFont = self.jsFont(fromFont: oskFont) ?? "undefined" } else { - kbDict = keyboardsDictionary[key] - } - - let kbVersion = latestKeyboardFileVersion(withID: keyboardID) ?? "1.0" - let isRTL = kbDict?[Key.keyboardRTL] - - var jsFont = jsFontString(font) - var jsOskFont: String - if oskFont == nil || oskFont == "''" { jsOskFont = jsFont - } else { - jsOskFont = jsFontString(oskFont) } - let escapedLangName = languageName!.replacingOccurrences(of: "'", with: "\\'") - let escapedKbName = keyboardName!.replacingOccurrences(of: "'", with: "\\'") - if jsFont == "''" { - jsFont = "undefined" - } - if jsOskFont == "''" { - jsOskFont = "undefined" - } - let jsString = "setKeymanLanguage('\(escapedKbName)','\(keyboardID)','\(escapedLangName)'," + - "'\(languageID)','\(kbVersion)',\(jsFont),\(jsOskFont))" + let escapedLangName = kb.languageName.replacingOccurrences(of: "'", with: "\\'") + let escapedKbName = kb.name.replacingOccurrences(of: "'", with: "\\'") + let jsString = """ + setKeymanLanguage('\(escapedKbName)','\(kb.id)','\(escapedLangName)',\ + '\(kb.languageID)','\(kbVersion)',\(jsFont),\(jsOskFont)) + """ kmLog("Evaluating JavaScript: \(jsString)", checkDebugPrinting: true) inputView.evaluateJavaScript(jsString, completionHandler: nil) let userData = isSystemKeyboard ? UserDefaults.standard : activeUserDefaults() - userData.set([ - Key.keyboardId: keyboardID, - Key.languageId: languageID, - Key.keyboardName: keyboardName, - Key.languageName: languageName, - Key.keyboardVersion: kbVersion, - Key.keyboardRTL: isRTL ?? "N", - Key.font: jsFont - ], forKey: Key.userCurrentKeyboard) + userData.set(kb, forKey: Key.userCurrentKeyboard) userData.synchronize() if isKeymanHelpOn { @@ -578,44 +309,12 @@ UIGestureRecognizerDelegate { perform(showHelpBubble, with: nil, afterDelay: 1.5) } - let kbInfo = [ - Key.keyboardId: keyboardID, - Key.languageId: languageID, - Key.keyboardName: escapedKbName, - Key.languageName: escapedLangName, - Key.keyboardVersion: kbVersion, - Key.font: jsFont - ] - - NotificationCenter.default.post(name: .keymanKeyboardChanged, object: self, - userInfo: [Key.keyboardInfo: kbInfo]) + NotificationCenter.default.post(name: .keymanKeyboardChanged, + object: self, + userInfo: [Key.keyboardInfo: kb]) return true } - public func setKeyboard(_ keyboard: [String: String]) -> Bool { - return setKeyboard(withID: keyboard[Key.keyboardId]!, - languageID: keyboard[Key.languageId]!, - keyboardName: keyboard[Key.keyboardName]!, - languageName: keyboard[Key.languageName]!, - font: keyboard[Key.font]!, - oskFont: keyboard[Key.oskFont]) - } - - private func jsFontString(_ font: String?) -> String { - guard let font = font, !font.isEmpty && (font.contains(".ttf") || font.contains(".otf")) else { - return "''" - } - - guard font.hasSuffix(".ttf") || font.hasSuffix(".otf") else { - return font - } - - let familyName = "font_family_\(font.dropLast(4))" - return """ - {"family":"\(familyName)","files":["\(font)"]} - """ - } - /// Adds a new keyboard to the list in the keyboard picker if it doesn't already exist. /// The keyboard must be downloaded (see `downloadKeyboard()`) or preloaded (see `preloadLanguageFile()`) /// @@ -624,44 +323,27 @@ UIGestureRecognizerDelegate { /// - isCustom: The keyboard is not provided by Keyman /// - font: Custom font for text views as a JSON String (see keyboardsDictionary) /// - oskFont: Font for the on-screen keyboard - public func addKeyboard(withID keyboardID: String, languageID: String, - keyboardName: String, languageName: String, - isRTL: Bool, isCustom: Bool, font: String?, oskFont: String?) { - // Check if keyboard file exists - guard let kbVersion = latestKeyboardFileVersion(withID: keyboardID) else { - kmLog("Could not add keyboard with ID: \(keyboardID) because the keyboard file does not exist", + public func addKeyboard(_ keyboard: InstallableKeyboard) { + if !keyboardFileExists(withID: keyboard.id, version: keyboard.version) { + kmLog("Could not add keyboard with ID: \(keyboard.id) because the keyboard file does not exist", checkDebugPrinting: false) return } // Get keyboards list if it exists in user defaults, otherwise create a new one - var userKeyboards = activeUserDefaults().array(forKey: Key.userKeyboardsList) as? [[String: String?]] ?? [] - - var keyboard = [ - Key.languageId: languageID, - Key.languageName: languageName, - Key.keyboardId: keyboardID, - Key.keyboardName: keyboardName, - Key.keyboardVersion: kbVersion, - Key.keyboardRTL: isRTL ? "Y" : "N", - Key.customKeyboard: isCustom ? "Y" : "N", - Key.font: font ?? "''" - ] - if let oskFont = oskFont { - keyboard[Key.oskFont] = oskFont - } + let userDefaults = activeUserDefaults() + var userKeyboards = userDefaults.installableKeyboards(forKey: Key.userKeyboardsList) ?? [] // Update keyboard if it exists - if let index = indexForUserKeyboard(withID: keyboardID, languageID: languageID) { + if let index = userKeyboards.index(where: { $0.id == keyboard.id && $0.languageID == keyboard.languageID }) { userKeyboards[index] = keyboard } else { userKeyboards.append(keyboard) } - let userData = activeUserDefaults() - userData.set(userKeyboards, forKey: Key.userKeyboardsList) - userData.set([Date()], forKey: Key.synchronizeSWKeyboard) - userData.synchronize() + userDefaults.set(userKeyboards, forKey: Key.userKeyboardsList) + userDefaults.set([Date()], forKey: Key.synchronizeSWKeyboard) + userDefaults.synchronize() } /// Removes a keyboard from the list in the keyboard picker if it exists. @@ -680,7 +362,7 @@ UIGestureRecognizerDelegate { let userData = activeUserDefaults() // If user defaults for keyboards list does not exist, do nothing. - guard var userKeyboards = userData.array(forKey: Key.userKeyboardsList) else { + guard var userKeyboards = userData.installableKeyboards(forKey: Key.userKeyboardsList) else { return false } @@ -688,14 +370,14 @@ UIGestureRecognizerDelegate { return false } - let kbDict = userKeyboards[index] + let kb = userKeyboards[index] userKeyboards.remove(at: index) userData.set(userKeyboards, forKey: Key.userKeyboardsList) userData.set([Date()], forKey: Key.synchronizeSWKeyboard) userData.synchronize() NotificationCenter.default.post(name: .keymanKeyboardRemoved, object: self, - userInfo: [Key.keyboardInfo: kbDict]) + userInfo: [Key.keyboardInfo: kb]) return true } @@ -706,102 +388,57 @@ UIGestureRecognizerDelegate { /// - Returns: The index of the keyboard if it exist in user keyboards list public func indexForUserKeyboard(withID keyboardID: String?, languageID: String?) -> Int? { - let userKeyboards = activeUserDefaults().array(forKey: Key.userKeyboardsList) - guard let keyboards = userKeyboards as? [[String: String]] else { - return nil - } - return keyboards.index { kb in keyboardID == kb[Key.keyboardId] && languageID == kb[Key.languageId] } + let userKeyboards = activeUserDefaults().installableKeyboards(forKey: Key.userKeyboardsList) + return userKeyboards?.index { $0.id == keyboardID && $0.languageID == languageID } + } + + public func userKeyboard(withID keyboardID: String, languageID: String) -> InstallableKeyboard? { + let userKeyboards = activeUserDefaults().installableKeyboards(forKey: Key.userKeyboardsList) + return userKeyboards?.first { $0.id == keyboardID && $0.languageID == languageID } + } + + public func repositoryKeyboard(withID keyboardID: String, languageID: String) -> InstallableKeyboard? { + return keyboardsDictionary["\(languageID)_\(keyboardID)"] } /// - Returns: Info for the current keyboard, if a keyboard is set - public var currentKeyboardInfo: [String: String]? { + public var currentKeyboardInfo: InstallableKeyboard? { guard let keyboardID = keyboardID, let languageID = languageID else { return nil } - - let userKeyboards = activeUserDefaults().array(forKey: Key.userKeyboardsList) as? [[String: String]] - let userKeyboard = userKeyboards?.first { kb in - return keyboardID == kb[Key.keyboardId] && languageID == kb[Key.languageId] - } - if let userKeyboard = userKeyboard { - return userKeyboard - } - - let key = "\(languageID)_\(keyboardID)" - if let kbInfo = keyboardsDictionary[key] { - return kbInfo - } - - return [ - Key.keyboardId: keyboardID, - Key.languageId: languageID - ] + return userKeyboard(withID: keyboardID, languageID: languageID) } /// Switch to the next keyboard. /// - Returns: Index of the newly selected keyboard. public func switchToNextKeyboard() -> Int? { - guard let index = currentKeyboardIndex else { + let userDefaults = activeUserDefaults() + guard let userKeyboards = userDefaults.installableKeyboards(forKey: Key.userKeyboardsList), + let index = userKeyboards.index(where: { isCurrentKeyboard($0) }) else { return nil } - - let userData = activeUserDefaults() - guard let userKeyboards = userData.array(forKey: Key.userKeyboardsList) as? [[String: String]] else { - return index - } - if userKeyboards.isEmpty { - return index - } - let newIndex = (index + 1) % userKeyboards.count setKeyboard(userKeyboards[newIndex]) return newIndex } - /// - Returns: Index of the current keyboard in the keyboard list. - public var currentKeyboardIndex: Int? { - let userData = activeUserDefaults() - guard let userKeyboards = userData.array(forKey: Key.userKeyboardsList) as? [[String: String]] else { - return nil - } - return userKeyboards.index { isCurrentKeyboard($0) } - } - func isCurrentKeyboard(withID keyboardID: String?, languageID: String?) -> Bool { return self.keyboardID == keyboardID && self.languageID == languageID } - func isCurrentKeyboard(_ keyboard: [String: Any]) -> Bool { - let keyboardID = keyboard[Key.keyboardId] as? String - let languageID = keyboard[Key.languageId] as? String - return keyboardID == self.keyboardID && languageID == self.languageID + func isCurrentKeyboard(_ keyboard: InstallableKeyboard) -> Bool { + return keyboard.id == self.keyboardID && keyboard.languageID == self.languageID } /// - Returns: The font name for the given keyboard ID and languageID, or returns nil if /// - The keyboard doesn't have a font /// - The keyboard info is not available in the user keyboards list or in keyboardsDictionary public func fontNameForKeyboard(withID keyboardID: String, languageID: String) -> String? { - let userData = activeUserDefaults() - let userKeyboards = userData.array(forKey: Key.userKeyboardsList) - - if let userKeyboards = userKeyboards as? [[String: String]] { - if let kb = userKeyboards.first(where: { isCurrentKeyboard($0) }) { - let font = kb[Key.font] - if let fontFilename = self.fontFilename(fromJSONFont: font!) { - return keymanFonts[fontFilename]?[Key.fontName] as? String - } - return nil - } + let kb = userKeyboard(withID: keyboardID, languageID: languageID) + ?? repositoryKeyboard(withID: keyboardID, languageID: languageID) + if let filename = kb?.font?.source.first(where: { $0.hasFontExtension }) { + return keymanFonts[filename]?[Key.fontName] as? String } - - if let kb = keyboardsDictionary["\(languageID)_\(keyboardID)"] { - let font = kb[Key.font] - if let fontFilename = self.fontFilename(fromJSONFont: font!) { - return keymanFonts[fontFilename]?[Key.fontName] as? String - } - return nil - } - return nil } @@ -809,87 +446,21 @@ UIGestureRecognizerDelegate { /// - The keyboard doesn't have an OSK font /// - The keyboard info is not available in the user keyboards list or in keyboardsDictionary func oskFontNameForKeyboard(withID keyboardID: String, languageID: String) -> String? { - let userData = activeUserDefaults() - let userKeyboards = userData.array(forKey: Key.userKeyboardsList) - - if let userKeyboards = userKeyboards as? [[String: String]] { - if let kb = userKeyboards.first(where: { isCurrentKeyboard($0) }) { - if let font = kb[Key.oskFont] { - let fontFilename = self.fontFilename(fromJSONFont: font) - return keymanFonts[fontFilename!]?[Key.fontName] as? String - } - } - } - - if let kb = keyboardsDictionary["\(languageID)_\(keyboardID)"] { - if let font = kb[Key.oskFont] { - let fontFilename = self.fontFilename(fromJSONFont: font) - return keymanFonts[fontFilename!]?[Key.fontName] as? String - } - } - - return nil - } - - func isRTLKeyboard(withID keyboardID: String, languageID: String) -> Bool { - // TODO: Refactor using a function that retrieves keyboard - let userData = activeUserDefaults() - if let userKeyboards = userData.array(forKey: Key.userKeyboardsList) as? [[String: String]] { - let keyboard = userKeyboards.first { keyboard in - return keyboard[Key.keyboardId] == keyboardID && keyboard[Key.languageId] == languageID - } - if let keyboard = keyboard { - return keyboard[Key.keyboardRTL] == "Y" - } - } - let kbKey = "\(languageID)_\(keyboardID)" - return keyboardsDictionary[kbKey]?[Key.keyboardRTL] == "Y" - } - - func fontFilename(fromJSONFont jsonFont: String) -> String? { - if jsonFont.hasFontExtension { - return jsonFont - } - - let jsonObject: Any - do { - jsonObject = try JSONSerialization.jsonObject(with: jsonFont.data(using: String.Encoding.utf8)!, - options: .mutableContainers) - } catch { - kmLog("Error decoding font JSON object: \(error)", checkDebugPrinting: false) - return nil - } - guard let fontDict = jsonObject as? [AnyHashable: Any] else { - return nil - } - let fontFiles = fontDict[Key.fontFiles] - if let fontFiles = fontFiles as? [String] { - if let fontFile = fontFiles.first(where: { $0.hasFontExtension }) { - return fontFile - } - } else if let fontFile = fontFiles as? String { - if fontFile.hasFontExtension { - return fontFile - } + let kb = userKeyboard(withID: keyboardID, languageID: languageID) + ?? repositoryKeyboard(withID: keyboardID, languageID: languageID) + if let filename = kb?.oskFont?.source.first(where: { $0.hasFontExtension }) { + return keymanFonts[filename]?[Key.fontName] as? String } return nil } - func jsFont(forKeyboardID kbID: String, languageID langID: String) -> String { - let kbKey = "\(langID)_\(kbID)" - let kbDict = keyboardsDictionary[kbKey] - let jsFont = kbDict?[Key.font] - return jsFont ?? "''" + func isRTLKeyboard(withID keyboardID: String, languageID: String) -> Bool? { + let kb = userKeyboard(withID: keyboardID, languageID: languageID) + ?? repositoryKeyboard(withID: keyboardID, languageID: languageID) + return kb?.isRTL } - func jsOskFont(forKeyboardID kbID: String, languageID langID: String) -> String { - let kbKey = "\(langID)_\(kbID)" - let kbDict = keyboardsDictionary[kbKey] - let jsOskFont = kbDict?[Key.oskFont] - return jsOskFont ?? "''" - } - - func jsFont(fromFont font: Font?) -> String { + func jsFont(fromFont font: Font?) -> String? { guard let font = font else { return jsFont(fromFontDictionary: nil) } @@ -900,9 +471,9 @@ UIGestureRecognizerDelegate { ]) } - func jsFont(fromFontDictionary fontDict: [AnyHashable: Any]?) -> String { + func jsFont(fromFontDictionary fontDict: [AnyHashable: Any]?) -> String? { guard let fontDict = fontDict, !fontDict.isEmpty else { - return "''" + return nil } let data: Data @@ -910,7 +481,7 @@ UIGestureRecognizerDelegate { data = try JSONSerialization.data(withJSONObject: fontDict, options: []) } catch { kmLog("Failed to encode font dictionary as JSON: \(String(describing: fontDict))", checkDebugPrinting: false) - return "''" + return nil } return String(data: data, encoding: .ascii)! @@ -954,61 +525,50 @@ UIGestureRecognizerDelegate { /// - Parameters: /// - isUpdate: Keep the keyboard files on failure public func downloadKeyboard(withID keyboardID: String, languageID: String, isUpdate: Bool) { - let kbInfo = [ - Key.keyboardId: keyboardID, - Key.languageId: languageID - ] + guard let keyboardsInfo = keyboardsInfo else { + let message = "Keyboard info has not yet been fetched. Call fetchKeyboardsList() first." + let error = NSError(domain: "Keyman", code: 0, userInfo: [NSLocalizedDescriptionKey: message]) + downloadFailed(forKeyboards: [], error: error) + return + } + + guard let keyboard = repositoryKeyboard(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]) + downloadFailed(forKeyboards: [], error: error) + return + } guard downloadQueue == nil else { - // Download queue is active. + let error = NSError(domain: "Keyman", code: 0, + userInfo: [NSLocalizedDescriptionKey: "Download queue is busy"]) + downloadFailed(forKeyboards: [keyboard], error: error) return } guard reachability.currentReachabilityStatus() != NotReachable else { let error = NSError(domain: "Keyman", code: 0, userInfo: [NSLocalizedDescriptionKey: "No internet connection"]) - downloadFailed(forKeyboard: kbInfo, error: error) - return - } - - guard let keyboardsInfo = keyboardsInfo else { - let message = "Keyboard info has not yet been fetched. Call fetchKeyboardsList() first." - let error = NSError(domain: "Keyman", code: 0, userInfo: [NSLocalizedDescriptionKey: message]) - downloadFailed(forKeyboard: kbInfo, error: error) - return - } - - let kbKey = "\(languageID)_\(keyboardID)" - guard let keyboardDict = keyboardsDictionary[kbKey] else { - let error = NSError(domain: "Keyman", code: 0, - userInfo: [NSLocalizedDescriptionKey: "Keyboard not found with key: \(kbKey)"]) - downloadFailed(forKeyboard: kbInfo, error: error) + downloadFailed(forKeyboards: [keyboard], error: error) return } let filename = keyboardsInfo[keyboardID]!.filename let keyboardURL = options!.keyboardBaseURL.appendingPathComponent(filename) - let kbFontStr = keyboardDict[Key.font] ?? "" - let kbOskFontStr = keyboardDict[Key.oskFont] ?? "" - var kbFontURLs: [URL] = [] - do { - kbFontURLs += try keyboardFontURLs(jsonString: kbFontStr) - kbFontURLs += try keyboardFontURLs(jsonString: kbOskFontStr) - } catch { - downloadFailed(forKeyboard: kbInfo, error: error as NSError) - return - } + let fontURLs = Array(Set(keyboardFontURLs(forFont: keyboard.font) + + keyboardFontURLs(forFont: keyboard.oskFont))) // TODO: Better typing downloadQueue = HTTPDownloader(self) let commonUserData: [String: Any] = [ - Key.keyboardInfo: kbInfo, - Key.update: isUpdate ? 1 : 0 + Key.keyboardInfo: [keyboard], + Key.update: isUpdate ] downloadQueue!.userInfo = commonUserData - let kbVersion = keyboardDict[Key.keyboardVersion] + let kbVersion = keyboard.version let keyboardPath = self.keyboardPath(forFilename: keyboardURL.lastPathComponent, keyboardVersion: kbVersion) @@ -1017,7 +577,7 @@ UIGestureRecognizerDelegate { request.tag = 0 downloadQueue!.addRequest(request) - for (i, url) in kbFontURLs.enumerated() { + for (i, url) in fontURLs.enumerated() { request = HTTPDownloadRequest(url: url, userInfo: commonUserData) request.destinationFile = fontPath(forFilename: url.lastPathComponent)?.path request.tag = i + 1 @@ -1026,31 +586,16 @@ UIGestureRecognizerDelegate { downloadQueue!.run() } - private func keyboardFontURLs(jsonString: String) throws -> [URL] { - let font = try? JSONSerialization.jsonObject(with: jsonString.data(using: String.Encoding.utf8)!, - options: .mutableContainers) - - guard let fontObject = font as? [AnyHashable: Any] else { + private func keyboardFontURLs(forFont font: Font?) -> [URL] { + guard let font = font else { return [] } - guard let baseURL = options?.fontBaseURL else { kmLog("Options not yet loaded", checkDebugPrinting: false) return [] } - - let fontFiles = fontObject[Key.fontFiles] - if let fontFiles = fontFiles as? [String] { - return fontFiles.filter({ $0.hasFontExtension }).map({ baseURL.appendingPathComponent($0) }) - } else if let source = fontFiles as? String { - if source.hasFontExtension { - return [baseURL.appendingPathComponent(source)] - } - return [] - } else { - let message = "Unexpected error: \(String(describing: fontFiles)) is not a valid type to be processed." - throw NSError(domain: "Keyman", code: 0, userInfo: [NSLocalizedDescriptionKey: message]) - } + return font.source.filter({ $0.hasFontExtension }) + .map({ baseURL.appendingPathComponent($0) }) } /// Downloads a custom keyboard from the URL @@ -1059,18 +604,10 @@ UIGestureRecognizerDelegate { /// - isDirect: The keyboard is downloaded directly instead of via Keyman Engine Cloud Services. /// Should normally be false to permit caching and prevent overloading a target server. public func downloadKeyboard(from jsonUrl: URL, isDirect: Bool) { - let kbInfo = [ - Key.keyboardId: "", - Key.languageId: "", - Key.keyboardName: "", - Key.languageName: "", - Key.customKeyboard: "Y" - ] - guard reachability.currentReachabilityStatus() != NotReachable else { let error = NSError(domain: "Keyman", code: 0, userInfo: [NSLocalizedDescriptionKey: "No connection"]) - downloadFailed(forKeyboard: kbInfo, error: error) + downloadFailed(forKeyboards: [], error: error) return } @@ -1080,183 +617,71 @@ UIGestureRecognizerDelegate { } else { let deviceParam = (UIDevice.current.userInterfaceIdiom == .phone) ? "iphone" : "ipad" let encodedUrl = jsonUrl.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)! - url = URL(string: "\(apiRemoteURL)\(encodedUrl)&device=\(deviceParam)")! + url = URL(string: "\(apiRemoteURL)\(encodedUrl)&dateformat=seconds&device=\(deviceParam)")! } guard let data = try? Data(contentsOf: url) else { let error = NSError(domain: "Keyman", code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to fetch JSON file"]) - downloadFailed(forKeyboard: kbInfo, error: error) + downloadFailed(forKeyboards: [], error: error) return } - let jsonObj: Any + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .secondsSince1970 do { - jsonObj = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) + let keyboard = try decoder.decode(Keyboard.self, from: data) + return downloadKeyboard(keyboard) } catch { - downloadFailed(forKeyboard: kbInfo, error: error as NSError) + downloadFailed(forKeyboards: [], error: error as NSError) return } - - guard let jsonDict = jsonObj as? [AnyHashable: Any] else { - let error = NSError(domain: "Keyman", code: 0, - userInfo: [NSLocalizedDescriptionKey: "The keyboard could not be installed"]) - downloadFailed(forKeyboard: kbInfo, error: error) - return - } - - downloadKeyboard(fromDictionary: jsonDict) } - /// Downloads a custom keyboard from dictionary - /// - Parameter jsonDict: Dictionary with the fields contained in a JSON keyboard file - func downloadKeyboard(fromDictionary jsonDict: [AnyHashable: Any]) { - - // TODO: parse JSON in a more type-safe manner - let options = jsonDict[Key.options] as? [AnyHashable: Any] - let kbBaseUri = options?[Key.keyboardBaseURI] as? String - let fontBaseUri = options?[Key.fontBaseURI] as? String - - guard let keyboard = jsonDict[Key.keyboard] as? [AnyHashable: Any], - let fileName = keyboard[Key.keyboardFilename] as? String, - let kbID = keyboard[Key.id] as? String, - let kbName = keyboard[Key.name] as? String, - let languages = keyboard[Key.languages] as? [[String: Any]] else { - let error = NSError(domain: "Keyman", code: 0, - userInfo: [NSLocalizedDescriptionKey: "The keyboard could not be installed"]) - let emptyKeyboard = [ - Key.keyboardId: "", - Key.languageId: "", - Key.keyboardName: "", - Key.languageName: "", - Key.keyboardVersion: "", - Key.keyboardRTL: "", - Key.customKeyboard: "Y" - ] - downloadFailed(forKeyboard: emptyKeyboard, error: error) - return - } - let kbVersion = keyboard[Key.keyboardVersion] as? String ?? "1.0" - let isRTL = keyboard[Key.keyboardRTL] as? String ?? "N" - let kbFont = keyboard[Key.font] as? [AnyHashable: Any] - let kbOskFont = keyboard[Key.oskFont] as? [AnyHashable: Any] - - let jsFont = self.jsFont(fromFontDictionary: kbFont) - let jsOskFont = kbOskFont.map { self.jsFont(fromFontDictionary: $0) } - - let keyboardInfos = languages.map { language -> [String: String] in - var info = [ - Key.keyboardId: kbID, - Key.languageId: language[Key.id] as! String, - Key.keyboardName: kbName, - Key.languageName: language[Key.name] as! String, - Key.keyboardVersion: kbVersion, - Key.keyboardRTL: isRTL, - Key.customKeyboard: "Y", - Key.font: jsFont - ] - if let jsOskFont = jsOskFont { - info[Key.oskFont] = jsOskFont - } - return info + /// Assumes that Keyboard has font and oskFont set and ignores fonts contained in Language. + private func downloadKeyboard(_ keyboard: Keyboard) { + let installableKeyboards = keyboard.languages!.map { language in + InstallableKeyboard(keyboard: keyboard, language: language) } - // TODO: Don't have the special case for a single keyboard info - let kbInfo: [String: Any] - if keyboardInfos.count == 1 { - kbInfo = [Key.keyboardInfo: keyboardInfos[0]] - } else { - kbInfo = [Key.keyboardInfo: keyboardInfos] - } + let filename = keyboard.filename + let keyboardURL = options!.keyboardBaseURL.appendingPathComponent(filename) + + let fontURLs = Array(Set(keyboardFontURLs(forFont: keyboard.font) + + keyboardFontURLs(forFont: keyboard.oskFont))) if downloadQueue != nil { // Download queue is active. let error = NSError(domain: "Keyman", code: 0, userInfo: [NSLocalizedDescriptionKey: "Download queue is busy"]) - downloadFailed(forKeyboard: kbInfo, error: error) + downloadFailed(forKeyboards: installableKeyboards, error: error) return } if reachability.currentReachabilityStatus() == NotReachable { let error = NSError(domain: "Keyman", code: 0, userInfo: [NSLocalizedDescriptionKey: "No internet connection"]) - downloadFailed(forKeyboard: kbInfo, error: error) + downloadFailed(forKeyboards: installableKeyboards, error: error) return } - let keyboardLocalPath = self.keyboardPath(forFilename: fileName, keyboardVersion: kbVersion)! - // FIXME: Maybe change to use appendingPathComponent - let keyboardURL = URL(string: kbBaseUri! + fileName)! - var keyboardFontURLs: [URL] = [] - // TODO: Refactor duplicate with kbOskFont - if let kbFont = kbFont, !kbFont.isEmpty { - // Font filename is deprecated - let fontSource = kbFont[Key.fontSource] ?? kbFont[Key.fontFilename] - if let fontSource = fontSource as? [String] { - keyboardFontURLs = fontSource.flatMap { source in - if source.hasFontExtension { - return URL(string: fontBaseUri! + source) - } else { - return nil - } - } - } else if let fontSource = fontSource as? String { - if fontSource.hasFontExtension { - keyboardFontURLs = [URL(string: fontBaseUri! + fontSource)!] - } else { - keyboardFontURLs = [] - } - } else { - let message = "Unexpected error: \(String(describing: fontSource)) is not a valid type to be processed." - let error = NSError(domain: "Keyman", code: 0, userInfo: [NSLocalizedDescriptionKey: message]) - downloadFailed(forKeyboard: kbInfo, error: error) - return - } - } - if let kbOskFont = kbOskFont, !kbOskFont.isEmpty { - // Font filename is deprecated - let fontSource = kbOskFont[Key.fontSource] ?? kbOskFont[Key.fontFilename] - if let fontSource = fontSource as? [String] { - keyboardFontURLs.append(contentsOf: fontSource.flatMap { source in - if source.hasFontExtension { - let url = URL(string: fontBaseUri! + source)! - if !keyboardFontURLs.contains(url) { - return url - } - } - return nil - }) - } else if let fontSource = fontSource as? String { - if fontSource.hasFontExtension { - let url = URL(string: fontBaseUri! + fontSource)! - if !keyboardFontURLs.contains(url) { - keyboardFontURLs.append(url) - } - } - } else { - let message = "Unexpected error: \(String(describing: fontSource)) is not a valid type to be processed." - let error = NSError(domain: "Keyman", code: 0, userInfo: [NSLocalizedDescriptionKey: message]) - downloadFailed(forKeyboard: kbInfo, error: error) - return - } - } - - let isUpdate = latestKeyboardFileVersion(withID: kbID) != nil + let keyboardLocalPath = self.keyboardPath(forFilename: filename, keyboardVersion: keyboard.version)! + let isUpdate = latestKeyboardFileVersion(withID: keyboard.id) != nil downloadQueue = HTTPDownloader.init(self) - let commonUserData = [ - Key.keyboardInfo: kbInfo[Key.keyboardInfo]!, - Key.update: (isUpdate ? 1 : 0) + let commonUserData: [String: Any] = [ + Key.keyboardInfo: installableKeyboards, + Key.update: isUpdate ] downloadQueue!.userInfo = commonUserData var request = HTTPDownloadRequest(url: keyboardURL, userInfo: commonUserData) request.destinationFile = keyboardLocalPath.path request.tag = 0 + downloadQueue!.addRequest(request) - for (i, url) in keyboardFontURLs.enumerated() { + for (i, url) in fontURLs.enumerated() { let fontPath = self.fontPath(forFilename: url.lastPathComponent)! - // FIXME: Downloading from local path to local path? - request = HTTPDownloadRequest(url: fontPath, userInfo: commonUserData) + request = HTTPDownloadRequest(url: url, userInfo: commonUserData) request.destinationFile = fontPath.path request.tag = i + 1 downloadQueue!.addRequest(request) @@ -1285,28 +710,15 @@ UIGestureRecognizerDelegate { } } - /// - Precondition: `options` and `languages` are set. + /// - Precondition: `languages` is set. private func createKeyboardsInfo() { let keyboardsWithID = languages.flatMap { language in language.keyboards!.map { kb in (kb.id, kb) } } keyboardsInfo = Dictionary(keyboardsWithID, uniquingKeysWith: { (old, _) in old }) - let keyboardsWithLanguage = languages.flatMap { language -> [(String, [String: String])] in + let keyboardsWithLanguage = languages.flatMap { language -> [(String, InstallableKeyboard)] in language.keyboards!.map { kb in - let key = "\(language.id)_\(kb.id)" - var dict = [ - Key.keyboardId: kb.id, - Key.languageId: language.id, - Key.keyboardName: kb.name, - Key.languageName: language.name, - Key.keyboardVersion: kb.version, - Key.keyboardRTL: kb.isRTL ? "Y" : "N", - Key.font: jsFont(fromFont: kb.font) - ] - if let oskFont = kb.oskFont { - dict[Key.oskFont] = jsFont(fromFont: oskFont) - } - return (key, dict) + return ("\(language.id)_\(kb.id)", InstallableKeyboard(keyboard: kb, language: language)) } } keyboardsDictionary = Dictionary(uniqueKeysWithValues: keyboardsWithLanguage) @@ -1325,21 +737,21 @@ UIGestureRecognizerDelegate { } userData.set(sdkVersion, forKey: Key.engineVersion) - guard var userKbList = userData.array(forKey: Key.userKeyboardsList) as? [[String: String]] else { + guard var userKbList = userData.installableKeyboards(forKey: Key.userKeyboardsList) else { kmLog("No user keyboards to update", checkDebugPrinting: true) return } for i in userKbList.indices { - let kbID = userKbList[i][Key.keyboardId]! - let langID = userKbList[i][Key.languageId]! - if var kb = keyboardsDictionary["\(langID)_\(kbID)"] { - kb[Key.keyboardVersion] = latestKeyboardFileVersion(withID: kbID) - kb[Key.customKeyboard] = "N" + let kbID = userKbList[i].id + let langID = userKbList[i].languageID + if var kb = repositoryKeyboard(withID: kbID, languageID: langID) { + kb.version = latestKeyboardFileVersion(withID: kbID)! + kb.isCustom = false userKbList[i] = kb } else { var kb = userKbList[i] - kb[Key.customKeyboard] = "Y" + kb.isCustom = true userKbList[i] = kb } } @@ -1350,15 +762,13 @@ UIGestureRecognizerDelegate { func keyboardIdForCurrentRequest() -> String? { if let currentRequest = currentRequest { let tmpStr = currentRequest.url.lastPathComponent - if tmpStr.hasSuffix(".js") { + if tmpStr.hasJavaScriptExtension { return String(tmpStr.dropLast(3)) } } else if let downloadQueue = downloadQueue { let kbInfo = downloadQueue.userInfo[Key.keyboardInfo] - if let dict = kbInfo as? [String: String] { - return dict[Key.keyboardId] - } else if let array = kbInfo as? [[String: String]] { - return array[0][Key.keyboardId] + if let keyboards = kbInfo as? [InstallableKeyboard], let keyboard = keyboards.first { + return keyboard.id } } return nil @@ -1404,20 +814,11 @@ UIGestureRecognizerDelegate { func downloadRequestFinished(_ request: HTTPDownloadRequest) { switch request.typeCode { case .downloadFile: - let kbInfo = request.userInfo[Key.keyboardInfo] - let keyboard: [String: String] - if let kb = kbInfo as? [String: String] { - keyboard = kb - } else if let keyboards = kbInfo as? [[String: String]], !keyboards.isEmpty { - keyboard = keyboards[0] - } else { - kmLog("downloadRequestFinished(): Unexpected userInfo: \(String(describing: kbInfo))", - checkDebugPrinting: false) - return - } - let kbID = keyboard[Key.keyboardId]! - let kbVersion = keyboard[Key.keyboardVersion] - let isUpdate = (request.userInfo[Key.update] as? Int)! != 0 + let keyboards = request.userInfo[Key.keyboardInfo] as! [InstallableKeyboard] + let keyboard = keyboards[0] + let kbID = keyboard.id + let kbVersion = keyboard.version + let isUpdate = request.userInfo[Key.update] as! Bool if let statusCode = request.responseStatusCode, statusCode == 200 { // The request has succeeded. @@ -1462,7 +863,7 @@ UIGestureRecognizerDelegate { } } } - downloadFailed(forKeyboard: request.userInfo, error: error) + downloadFailed(forKeyboards: keyboards, error: error) } case .downloadCachedData: if request == currentRequest { @@ -1502,23 +903,14 @@ UIGestureRecognizerDelegate { switch request.typeCode { case .downloadFile: downloadQueue = nil - let error = request.error ?? NSError(domain: "Keyman", code: 0, userInfo: nil) - + let error = request.error! kmLog("Keyboard download failed: \(error).", checkDebugPrinting: true) - let kbInfo = request.userInfo[Key.keyboardInfo] - let keyboard: [String: String] - if let kb = kbInfo as? [String: String] { - keyboard = kb - } else if let keyboards = kbInfo as? [[String: String]], !keyboards.isEmpty { - keyboard = keyboards[0] - } else { - kmLog("downloadRequestFailed(): Unexpected userInfo: \(String(describing: kbInfo))", - checkDebugPrinting: false) - return - } - let kbID = keyboard[Key.keyboardId]! - let kbVersion = keyboard[Key.keyboardVersion] - let isUpdate = (request.userInfo[Key.update] as? Int)! != 0 + + let keyboards = request.userInfo[Key.keyboardInfo] as! [InstallableKeyboard] + let keyboard = keyboards[0] + let kbID = keyboard.id + let kbVersion = keyboard.version + let isUpdate = request.userInfo[Key.update] as! Bool if !isUpdate { let fileManager = FileManager.default @@ -1535,7 +927,7 @@ UIGestureRecognizerDelegate { } } } - downloadFailed(forKeyboard: request.userInfo, error: error as NSError) + downloadFailed(forKeyboards: keyboards, error: error as NSError) case .downloadCachedData: if request == currentRequest { let error = request.error! @@ -1552,13 +944,12 @@ UIGestureRecognizerDelegate { } } - func downloadFailed(forKeyboard keyboardInfo: [AnyHashable: Any], error: NSError) { - let kbInfo = keyboardInfo[Key.keyboardInfo] ?? keyboardInfo - NotificationCenter.default.post(name: .keymanKeyboardDownloadFailed, object: self, - userInfo: [ - Key.keyboardInfo: kbInfo, - NSUnderlyingErrorKey: error - ]) + private func downloadFailed(forKeyboards keyboards: [InstallableKeyboard], error: NSError) { + let userInfo: [AnyHashable: Any] = [ + NSUnderlyingErrorKey: error, + Key.keyboardInfo: keyboards + ] + NotificationCenter.default.post(name: .keymanKeyboardDownloadFailed, object: self, userInfo: userInfo) } // MARK: - Loading custom keyboards @@ -1714,7 +1105,7 @@ UIGestureRecognizerDelegate { // TODO: Use a logging library or have more than 2 log levels // Facilitates KeymanEngine internal logging. - func kmLog(_ logStr: String, checkDebugPrinting: Bool) { + public func kmLog(_ logStr: String, checkDebugPrinting: Bool) { if checkDebugPrinting && !isDebugPrintingOn { return } @@ -1804,8 +1195,8 @@ UIGestureRecognizerDelegate { try? fileManager.removeItem(at: fileUrl) let userData = activeUserDefaults() - let curKB = userData.object(forKey: Key.userCurrentKeyboard) as? [AnyHashable: Any] - if curKB?[Key.keyboardId] as? String == "us" { + let curKB = userData.installableKeyboard(forKey: Key.userCurrentKeyboard) + if curKB?.id == "us" { userData.removeObject(forKey: Key.userCurrentKeyboard) userData.synchronize() } @@ -2066,6 +1457,14 @@ UIGestureRecognizerDelegate { return activeFontDirectory()?.appendingPathComponent(filename) } + func keyboardFileExists(withID keyboardID: String, version: String) -> Bool { + guard let langDir = activeLanguageDirectory() else { + return false + } + let path = langDir.appendingPathComponent("\(keyboardID)-\(version).js").path + return FileManager.default.fileExists(atPath: path) + } + func latestKeyboardFileVersion(withID keyboardID: String) -> String? { guard let langDirPath = activeLanguageDirectory()?.path else { return nil @@ -2123,15 +1522,15 @@ UIGestureRecognizerDelegate { func updateKeyboardVersion(forID kbID: String, newKeyboardVersion kbVersion: String) { let userData = activeUserDefaults() - guard var userKeyboards = userData.array(forKey: Key.userKeyboardsList) as? [[String: String]] else { + guard var userKeyboards = userData.installableKeyboards(forKey: Key.userKeyboardsList) else { return } // Set version in user keyboards list for i in userKeyboards.indices { var kb = userKeyboards[i] - if kbID == kb[Key.keyboardId] { - kb[Key.keyboardVersion] = kbVersion + if kbID == kb.id { + kb.version = kbVersion userKeyboards[i] = kb } } @@ -2141,9 +1540,9 @@ UIGestureRecognizerDelegate { // Set version for current keyboard // TODO: Move this UserDefaults into a function let currentUserData = isSystemKeyboard ? UserDefaults.standard : activeUserDefaults() - if var userKb = currentUserData.object(forKey: Key.userCurrentKeyboard) as? [String: String] { - if kbID == userKb[Key.keyboardId] { - userKb[Key.keyboardVersion] = kbVersion + if var userKb = currentUserData.installableKeyboard(forKey: Key.userCurrentKeyboard) { + if kbID == userKb.id { + userKb.version = kbVersion currentUserData.set(userKb, forKey: Key.userCurrentKeyboard) currentUserData.synchronize() } @@ -2339,19 +1738,16 @@ UIGestureRecognizerDelegate { } @objc func resetKeyboard() { - let index = indexForUserKeyboard(withID: keyboardID, languageID: languageID) + let keyboard = currentKeyboardInfo keyboardID = nil languageID = nil - let userKeyboards = activeUserDefaults().array(forKey: Key.userKeyboardsList) as? [[String: String]] - if let index = index { - setKeyboard(userKeyboards![index]) - } else if let userKeyboards = userKeyboards, !userKeyboards.isEmpty { - setKeyboard(userKeyboards[0]) + if let keyboard = keyboard { + setKeyboard(keyboard) + } else if let keyboard = activeUserDefaults().installableKeyboards(forKey: Key.userKeyboardsList)?[safe: 0] { + setKeyboard(keyboard) } else { - setKeyboard(withID: DefaultKeyboard.keyboardID, languageID: DefaultKeyboard.languageID, - keyboardName: DefaultKeyboard.keyboardName, languageName: DefaultKeyboard.languageName, - font: DefaultKeyboard.keyboardFont, oskFont: nil) + setKeyboard(Constants.defaultKeyboard) } } @@ -2556,22 +1952,16 @@ UIGestureRecognizerDelegate { let deviceType = (UIDevice.current.userInterfaceIdiom == .phone) ? "AppleMobile" : "AppleTablet" webView.evaluateJavaScript("setDeviceType('\(deviceType)');", completionHandler: nil) if (keyboardID == nil || languageID == nil) && !shouldReloadKeyboard { - var newKb = DefaultKeyboard.keyboard + var newKb = Constants.defaultKeyboard let userData = isSystemKeyboard ? UserDefaults.standard : activeUserDefaults() - if let currentKb = userData.dictionary(forKey: Key.userCurrentKeyboard) as? [String: String] { - let kbID = currentKb[Key.keyboardId] - let langID = currentKb[Key.languageId] + if let currentKb = userData.installableKeyboard(forKey: Key.userCurrentKeyboard) { + let kbID = currentKb.id + let langID = currentKb.languageID if userKeyboardExists(withID: kbID, languageID: langID) { newKb = currentKb - if let kbName = currentKb[Key.keyboardName]?.replacingOccurrences(of: "\\'", with: "\'") { - newKb[Key.keyboardName] = kbName - } - if let langName = currentKb[Key.languageName]?.replacingOccurrences(of: "\\'", with: "\'") { - newKb[Key.languageName] = langName - } } - } else if let userKbs = activeUserDefaults().array(forKey: Key.userKeyboardsList) as? [[String: String]], + } else if let userKbs = activeUserDefaults().installableKeyboards(forKey: Key.userKeyboardsList), !userKbs.isEmpty { newKb = userKbs[0] } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Model/InstallableKeyboard.swift b/ios/engine/KMEI/KeymanEngine/Classes/Model/InstallableKeyboard.swift index fc09b34a37..d98681108e 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Model/InstallableKeyboard.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Model/InstallableKeyboard.swift @@ -10,15 +10,15 @@ import Foundation /// Mainly differs from the API `Keyboard` by having an associated language. public struct InstallableKeyboard: Codable { - public let id: String - public let name: String - public let languageID: String - public let languageName: String - public let version: String - public let isRTL: Bool - public let font: Font? - public let oskFont: Font? - public let isCustom: Bool + public var id: String + public var name: String + public var languageID: String + public var languageName: String + public var version: String + public var isRTL: Bool + public var font: Font? + public var oskFont: Font? + public var isCustom: Bool public init(id: String, name: String, diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Model/Keyboard.swift b/ios/engine/KMEI/KeymanEngine/Classes/Model/Keyboard.swift index 0d017642dc..58f36c3c8e 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Model/Keyboard.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Model/Keyboard.swift @@ -89,6 +89,7 @@ public struct Keyboard: Codable { let filename = try container.decode(String.self, forKey: .filename) let isDefault = try container.decodeIfPresent(Bool.self, forKey: .isDefault) let isRTL = try container.decodeIfPresent(Bool.self, forKey: .isRTL) + // TODO: Handle both seconds and ISO 8601 let lastModified = try container.decode(Date.self, forKey: .lastModified) let fileSize = try container.decode(Int.self, forKey: .fileSize) let version = try container.decode(String.self, forKey: .version) diff --git a/ios/engine/KMEI/KeymanEngine/Classes/TextField.swift b/ios/engine/KMEI/KeymanEngine/Classes/TextField.swift index 47a7326f9d..d8dc2fb8db 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/TextField.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/TextField.swift @@ -254,7 +254,7 @@ public class TextField: UITextField, UITextFieldDelegate, KeymanWebViewDelegate let isRTL: Bool if let keyboardID = Manager.shared.keyboardID, let languageID = Manager.shared.languageID { - isRTL = Manager.shared.isRTLKeyboard(withID: keyboardID, languageID: languageID) + isRTL = Manager.shared.isRTLKeyboard(withID: keyboardID, languageID: languageID) ?? false } else { isRTL = false } @@ -330,10 +330,9 @@ public class TextField: UITextField, UITextFieldDelegate, KeymanWebViewDelegate return } - let kbInfo = notification.userInfo?[Key.keyboardInfo] as? [AnyHashable: Any] - let keyboardID = kbInfo?[Key.keyboardId] as? String - let languageID = kbInfo?[Key.languageId] as? String - let fontName = Manager.shared.fontNameForKeyboard(withID: keyboardID!, languageID: languageID!) + // TODO: Get font name directly from keyboard object + let kb = notification.userInfo?[Key.keyboardInfo] as! InstallableKeyboard + let fontName = Manager.shared.fontNameForKeyboard(withID: kb.id, languageID: kb.languageID) let fontSize = font?.pointSize ?? UIFont.systemFontSize if let fontName = fontName { font = UIFont(name: fontName, size: fontSize) diff --git a/ios/engine/KMEI/KeymanEngine/Classes/TextView.swift b/ios/engine/KMEI/KeymanEngine/Classes/TextView.swift index 9967a6a138..4b2567513b 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/TextView.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/TextView.swift @@ -233,7 +233,7 @@ public class TextView: UITextView, UITextViewDelegate, UIInputViewAudioFeedback, let isRTL: Bool if let keyboardID = Manager.shared.keyboardID, let languageID = Manager.shared.languageID { - isRTL = Manager.shared.isRTLKeyboard(withID: keyboardID, languageID: languageID) + isRTL = Manager.shared.isRTLKeyboard(withID: keyboardID, languageID: languageID) ?? false } else { isRTL = false } @@ -323,10 +323,9 @@ public class TextView: UITextView, UITextViewDelegate, UIInputViewAudioFeedback, return } - let kbInfo = notification.userInfo?[Key.keyboardInfo] as? [AnyHashable: Any] - let keyboardID = kbInfo?[Key.keyboardId] as? String - let languageID = kbInfo?[Key.languageId] as? String - let fontName = Manager.shared.fontNameForKeyboard(withID: keyboardID!, languageID: languageID!) + // TODO: Get font name directly from keyboard + let kb = notification.userInfo?[Key.keyboardInfo] as! InstallableKeyboard + let fontName = Manager.shared.fontNameForKeyboard(withID: kb.id, languageID: kb.languageID) let fontSize = font?.pointSize ?? UIFont.systemFontSize if let fontName = fontName { font = UIFont(name: fontName, size: fontSize) diff --git a/ios/engine/KMEI/KeymanEngineDemo/MainViewController.swift b/ios/engine/KMEI/KeymanEngineDemo/MainViewController.swift index b3b3b61add..61d687e7d9 100644 --- a/ios/engine/KMEI/KeymanEngineDemo/MainViewController.swift +++ b/ios/engine/KMEI/KeymanEngineDemo/MainViewController.swift @@ -213,59 +213,30 @@ class MainViewController: UIViewController, UIAlertViewDelegate, TextViewDelegat // MARK: - Responding to Keyman notifications @objc func keyboardDownloadStarted(_ notification: Notification) { - let kbInfo = notification.userInfo?[Key.keyboardInfo] as? [AnyHashable: Any] - let kbID = kbInfo?[Key.keyboardId] as? String - let langID = kbInfo?[Key.languageId] as? String - let kbName = kbInfo?[Key.keyboardName] as? String - let langName = kbInfo?[Key.languageName] as? String - if kbID != nil && langID != nil && kbName != nil && langName != nil { - showActivityIndicator() - } + showActivityIndicator() } @objc func keyboardDownloaded(_ notification: Notification) { // This is an example of responding to a Keyman event. // - for a list of all events, see KMManager.h - let kbInfo = notification.userInfo?[Key.keyboardInfo] as? [AnyHashable: Any] - let kbID = kbInfo?[Key.keyboardId] as? String - let langID = kbInfo?[Key.languageId] as? String - let kbName = kbInfo?[Key.keyboardName] as? String - let langName = kbInfo?[Key.languageName] as? String - let isRTL = (kbInfo?[Key.keyboardRTL] as? String) == "Y" - let isCustom = (kbInfo?[Key.customKeyboard] as? String) == "Y" - let font = kbInfo?[Key.font] as? String - let oskFont = kbInfo?[Key.oskFont] as? String - - if let kbID = kbID, - let langID = langID, - let kbName = kbName, - let langName = langName { - Manager.shared.addKeyboard(withID: kbID, languageID: langID, keyboardName: kbName, - languageName: langName, isRTL: isRTL, isCustom: isCustom, - font: font, oskFont: oskFont) - Manager.shared.setKeyboard(withID: kbID, languageID: langID, keyboardName: kbName, - languageName: langName, font: font, oskFont: oskFont) - perform(#selector(self.dismissActivityIndicator), with: nil, afterDelay: 1.0) + let keyboards = notification.userInfo![Key.keyboardInfo] as! [InstallableKeyboard] + for keyboard in keyboards { + Manager.shared.addKeyboard(keyboard) + Manager.shared.setKeyboard(keyboard) } + perform(#selector(self.dismissActivityIndicator), with: nil, afterDelay: 1.0) } @objc func keyboardDownloadFailed(_ notification: Notification) { - let kbInfo = notification.userInfo?[Key.keyboardInfo] as? [AnyHashable: Any] - let kbID = kbInfo?[Key.keyboardId] as? String - let langID = kbInfo?[Key.languageId] as? String - let kbName = kbInfo?[Key.keyboardName] as? String - let langName = kbInfo?[Key.languageName] as? String - if kbID != nil && langID != nil && kbName != nil && langName != nil { - guard let error = notification.userInfo?[NSUnderlyingErrorKey] as? Error else { - return - } - if error.localizedDescription != "Download queue is busy" { - perform(#selector(self.dismissActivityIndicator), with: nil, afterDelay: 1.0) - perform(#selector(self.showAlert), with: error.localizedDescription, afterDelay: 1.1) - } else { - showAlert(error.localizedDescription) - } + guard let error = notification.userInfo?[NSUnderlyingErrorKey] as? Error else { + return + } + if error.localizedDescription != "Download queue is busy" { + perform(#selector(self.dismissActivityIndicator), with: nil, afterDelay: 1.0) + perform(#selector(self.showAlert), with: error.localizedDescription, afterDelay: 1.1) + } else { + showAlert(error.localizedDescription) } }