From c7bb65015dc663163beb7fce6aff049f896fc060 Mon Sep 17 00:00:00 2001 From: jahorton Date: Wed, 18 Dec 2019 10:51:41 +0700 Subject: [PATCH 1/3] refactor(ios): creates separate class for adhoc kmp install prompt --- .../KeymanEngine.xcodeproj/project.pbxproj | 20 ++- .../PackageInstallViewController.swift | 84 ++++++++++ .../ResourceDownloadManager.swift | 0 .../ResourceDownloadQueue.swift | 0 .../ResourceFileManager.swift | 155 ++++++++++++++++++ ios/keyman/Keyman/Keyman/AppDelegate.swift | 83 ++-------- 6 files changed, 270 insertions(+), 72 deletions(-) create mode 100644 ios/engine/KMEI/KeymanEngine/Classes/Resource Management/PackageInstallViewController.swift rename ios/engine/KMEI/KeymanEngine/Classes/{KeyboardRepository => Resource Management}/ResourceDownloadManager.swift (100%) rename ios/engine/KMEI/KeymanEngine/Classes/{KeyboardRepository => Resource Management}/ResourceDownloadQueue.swift (100%) create mode 100644 ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift diff --git a/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj b/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj index 876a56dbf4..aafd752f36 100644 --- a/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj +++ b/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj @@ -210,6 +210,8 @@ CE2B1E4821B60E8A007D092E /* DeviceKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2B1E4521B60E7C007D092E /* DeviceKit.framework */; }; CE2B1E4A21B60FB1007D092E /* DeviceKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CE2B1E4521B60E7C007D092E /* DeviceKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; CE67D961228A6F190029F2B5 /* KeyboardCommandStructs.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE67D960228A6F190029F2B5 /* KeyboardCommandStructs.swift */; }; + CE71705823A9C14D00A924A1 /* ResourceFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE71705723A9C14D00A924A1 /* ResourceFileManager.swift */; }; + CE71705F23A9C97F00A924A1 /* PackageInstallViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE71705E23A9C97F00A924A1 /* PackageInstallViewController.swift */; }; CE808A48236697BE00713E6B /* DeviceKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2B1E4521B60E7C007D092E /* DeviceKit.framework */; }; CE808A4B236697D400713E6B /* ObjcExceptionBridging.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1687ACCD1FD8DE5300926D69 /* ObjcExceptionBridging.framework */; }; CE808A4D236697D500713E6B /* Reachability.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A0FC9FC22D66D9E00D33F86 /* Reachability.framework */; }; @@ -414,6 +416,8 @@ CE24ECEF21B763740052D291 /* KeymanResponder+Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KeymanResponder+Types.swift"; sourceTree = ""; }; CE2B1E4521B60E7C007D092E /* DeviceKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = DeviceKit.framework; path = ../../Carthage/Build/iOS/DeviceKit.framework; sourceTree = ""; }; CE67D960228A6F190029F2B5 /* KeyboardCommandStructs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardCommandStructs.swift; sourceTree = ""; }; + CE71705723A9C14D00A924A1 /* ResourceFileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourceFileManager.swift; sourceTree = ""; }; + CE71705E23A9C97F00A924A1 /* PackageInstallViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackageInstallViewController.swift; sourceTree = ""; }; CECB38931F2199BC0098882F /* Reachability.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Reachability.h; path = KeymanEngine/lib/Reachability/Reachability.h; sourceTree = SOURCE_ROOT; }; CECB38941F2199BC0098882F /* Reachability.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Reachability.m; path = KeymanEngine/lib/Reachability/Reachability.m; sourceTree = SOURCE_ROOT; }; F243887E14BBD43000A3E055 /* KeymanEngineDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = KeymanEngineDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -714,8 +718,6 @@ 9A9CB0852241717C00231FB9 /* LexicalModelRepositoryDelegate.swift */, C07A9D8D1FD1798900828ADD /* APIKeyboardRepository.swift */, 9A9CB0812241704800231FB9 /* APILexicalModelRepository.swift */, - CE1F67A22304EB3800FF6972 /* ResourceDownloadManager.swift */, - CE22DFB9230B94DB00A4551C /* ResourceDownloadQueue.swift */, ); path = KeyboardRepository; sourceTree = ""; @@ -738,6 +740,17 @@ name = KeymanWebView; sourceTree = ""; }; + CE71705923A9C7D300A924A1 /* Resource Management */ = { + isa = PBXGroup; + children = ( + CE1F67A22304EB3800FF6972 /* ResourceDownloadManager.swift */, + CE22DFB9230B94DB00A4551C /* ResourceDownloadQueue.swift */, + CE71705723A9C14D00A924A1 /* ResourceFileManager.swift */, + CE71705E23A9C97F00A924A1 /* PackageInstallViewController.swift */, + ); + path = "Resource Management"; + sourceTree = ""; + }; F243887314BBD43000A3E055 = { isa = PBXGroup; children = ( @@ -825,6 +838,7 @@ F273AB9615641D9300A47CEE /* Classes */ = { isa = PBXGroup; children = ( + CE71705923A9C7D300A924A1 /* Resource Management */, 165EB39F2098992D00040A69 /* Errors */, C055E6E81F99EA320035C2DD /* Extension */, C0452BA91F9F1CAF0064431A /* Model */, @@ -1252,6 +1266,7 @@ C0324B931F87689B00AF3785 /* KeymanURLProtocol.swift in Sources */, C05F43311FBD62550058CBD4 /* JSONDecoder.DateDecodingStrategy+ISO8601Fallback.swift in Sources */, C0452BAB1F9F1FE10064431A /* Language.swift in Sources */, + CE71705F23A9C97F00A924A1 /* PackageInstallViewController.swift in Sources */, C06D37431F81F5C400F61AE0 /* KeyboardPickerBarButtonItem.swift in Sources */, C082CE151F90AFD400860F02 /* Collection+SafeAccess.swift in Sources */, C06D37441F81F5C400F61AE0 /* KeyboardNameTableViewCell.swift in Sources */, @@ -1260,6 +1275,7 @@ C08E69911FDA6F6F0026056B /* FullKeyboardID.swift in Sources */, 9A9CB0822241704800231FB9 /* APILexicalModelRepository.swift in Sources */, C040E5101F8606E300901EE4 /* TextField.swift in Sources */, + CE71705823A9C14D00A924A1 /* ResourceFileManager.swift in Sources */, 9A082559227589360051EBB0 /* Formatter+ISODateExtension.swift in Sources */, C0324B8D1F87480700AF3785 /* TextFieldDelegateProxy.swift in Sources */, C075EB061F8EFF870041F4BD /* String+Helpers.swift in Sources */, diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/PackageInstallViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/PackageInstallViewController.swift new file mode 100644 index 0000000000..f67d4c2a01 --- /dev/null +++ b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/PackageInstallViewController.swift @@ -0,0 +1,84 @@ +// +// PackageInstallViewController.swift +// KeymanEngine +// +// Created by Joshua Horton on 12/18/19. +// Copyright © 2019 SIL International. All rights reserved. +// + +import Foundation +import WebKit + +public class PackageInstallViewController: UIViewController { + public typealias CompletionHandler = (Error?) -> Void + + let package: KeymanPackage + var wkWebView: WKWebView? + let completionHandler: CompletionHandler + + public init(for package: KeymanPackage, completionHandler: @escaping CompletionHandler) { + self.package = package + self.completionHandler = completionHandler + super.init(nibName: nil, bundle: nil) + + _ = view + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func loadView() { + wkWebView = WKWebView.init(frame: .zero) + wkWebView!.backgroundColor = .white + view = wkWebView! + + // Ensure the web view fills its available space. + wkWebView?.autoresizingMask = [.flexibleWidth, .flexibleHeight] + + let cancelBtn = UIBarButtonItem(title: "Cancel", style: .plain, + target: self, + action: #selector(cancelBtnHandler)) + let installBtn = UIBarButtonItem(title: "Install", style: .plain, + target: self, + action: #selector(installBtnHandler)) + + navigationItem.leftBarButtonItem = cancelBtn + navigationItem.rightBarButtonItem = installBtn + } + + override public func viewWillAppear(_ animated: Bool) { + wkWebView?.loadHTMLString(package.infoHtml(), baseURL: nil) + } + + @objc func cancelBtnHandler() { + dismiss(animated: true, completion: nil) + } + + @objc func installBtnHandler() { + dismiss(animated: true, completion: { + let package = self.package + do { + // Time to pass the package off to the final installers - the parse__KMP methods. + // ... they should probably be moved to ResourceFileManager eventually. + if package.isKeyboard() { + try Manager.shared.parseKbdKMP(package.sourceFolder) + } else { + try Manager.parseLMKMP(package.sourceFolder) + } + self.completionHandler(nil) + } catch { + log.error(error as! KMPError) + self.completionHandler(error) + } + + //this can fail gracefully and not show errors to users + do { + try FileManager.default.removeItem(at: package.sourceFolder) + } catch { + log.error("unable to delete temp files: \(error)") + self.completionHandler(error) + } + }) + } +} diff --git a/ios/engine/KMEI/KeymanEngine/Classes/KeyboardRepository/ResourceDownloadManager.swift b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceDownloadManager.swift similarity index 100% rename from ios/engine/KMEI/KeymanEngine/Classes/KeyboardRepository/ResourceDownloadManager.swift rename to ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceDownloadManager.swift diff --git a/ios/engine/KMEI/KeymanEngine/Classes/KeyboardRepository/ResourceDownloadQueue.swift b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceDownloadQueue.swift similarity index 100% rename from ios/engine/KMEI/KeymanEngine/Classes/KeyboardRepository/ResourceDownloadQueue.swift rename to ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceDownloadQueue.swift diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift new file mode 100644 index 0000000000..15daeaba98 --- /dev/null +++ b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift @@ -0,0 +1,155 @@ +//// +//// ResourceManager.swift +//// KeymanEngine +//// +//// Created by Joshua Horton on 12/18/19. +//// Copyright © 2019 SIL International. All rights reserved. +//// +// +//import Foundation +// +///** +// * This class stores common methods used for installing language resources, regardless of source. +// * +// * It also contains methods for general-purpose installation of language resources from .kmp files. +// */ +//public class ResourceFileManager { +// public static let shared = ResourceFileManager() +// +// fileprivate init() { +// } +// +// /** +// * Use this function to "install" external KMP files to within the Keyman app's alloted iOS file management domain. +// * Note that we don't request permissions to support opening/modifying files "in place," so we need to copy .kmps +// * before unzipping them. +// * +// * This implementation does not change how files are managed by the app; only where the file management code +// * is located. +// */ +// public func installFile(_ url: URL) { +// // Once selected, start the standard install process. +// log.info("Installing KMP at \(url)") +// +// // Step 1: Copy it to within the app's controlled space, making it a .zip in the process +// var destinationUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] +// destinationUrl.appendPathComponent("\(url.lastPathComponent).zip") +// +// do { +// let fileManager = FileManager.default +// +// // For now, we'll always allow overwriting. +// if fileManager.fileExists(atPath: destinationUrl.path) { +// try fileManager.removeItem(at: destinationUrl) +// } +// +// // Throws an error if the destination file already exists, and there's no +// // built-in override parameter. Hence, the previous if-block. +// try fileManager.copyItem(at: url, to: destinationUrl) +// installAdhocKeyboard(url: destinationUrl) +// return true +// } catch { +// showKMPError(KMPError.copyFiles) +// log.error(error) +// return +// } +// +// // Now, install it as if we'd just downloaded it. +// let resourceManager = ResourceDownloadManager.shared +// // Obviously, a BIG assumption here: that it's a lexical model, not a keyboard. +// // Pardon the proof of concept. +// guard let lexicalModels = resourceManager.installLexicalModelPackage(at: destinationUrl) else { +// log.info("Could not install KMP at \(url)") +// return +// } +// +// log.info("Attempt success!") +// } +// +// private func installAdhocKeyboard(url: URL) { +// let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] +// var destination = documentsDirectory +// destination.appendPathComponent("temp/\(url.lastPathComponent)") +// +// KeymanPackage.extract(fileUrl: url, destination: destination, complete: { kmp in +// if let kmp = kmp { +// self.promptAdHocInstall(kmp) +// } else { +// self.showKMPError(KMPError.invalidPackage) +// } +// }) +// } +// +// public func promptAdHocInstall(_ kmp: KeymanPackage) { +// _adhocDirectory = kmp.sourceFolder +// let isKbd = kmp.isKeyboard() +// +// let vc = UIViewController() +// vc.view.backgroundColor = .red +// let wkWebView = WKWebView.init(frame: vc.view.frame) +// wkWebView.backgroundColor = .white +// vc.view.addSubview(wkWebView) +// let cancelBtn = UIBarButtonItem(title: "Cancel", style: .plain, +// target: self, +// action: #selector(cancelAdHocBtnHandler)) +// let installBtn = UIBarButtonItem(title: "Install", style: .plain, +// target: self, +// action: (isKbd ? #selector(installAdHocKeyboardBtnHandler) : +// #selector(installAdHocLexicalModelBtnHandler)) ) +// vc.navigationItem.leftBarButtonItem = cancelBtn +// vc.navigationItem.rightBarButtonItem = installBtn +// let nvc = UINavigationController.init(rootViewController: vc) +// +// self.window?.rootViewController?.present(nvc, animated: true, completion: { +// wkWebView.loadHTMLString(kmp.infoHtml(), baseURL: nil) +// }) +// } +// +// @objc func installAdHocKeyboardBtnHandler() { +// if let adhocDir = _adhocDirectory { +// self.window?.rootViewController?.dismiss(animated: true, completion: { +// do { +// try Manager.shared.parseKbdKMP(adhocDir) +// self.showSimpleAlert(title: "Success", message: "Installed successfully.") +// } catch { +// self.showKMPError(error as! KMPError) +// } +// +// //this can fail gracefully and not show errors to users +// do { +// try FileManager.default.removeItem(at: adhocDir) +// } catch { +// log.error("unable to delete temp files") +// } +// }) +// } +// } +// +// @objc func installAdHocLexicalModelBtnHandler() { +// if let adhocDir = _adhocDirectory { +// self.window?.rootViewController?.dismiss(animated: true, completion: { +// do { +// try Manager.parseLMKMP(adhocDir) +// self.showSimpleAlert(title: "Success", message: "Installed successfully.") +// } catch { +// self.showKMPError(error as! KMPError) +// } +// +// //this can fail gracefully and not show errors to users +// do { +// try FileManager.default.removeItem(at: adhocDir) +// } catch { +// log.error("unable to delete temp files") +// } +// }) +// } +// } +// +// @objc func cancelAdHocBtnHandler() { +// self.window?.rootViewController?.dismiss(animated: true, completion: nil) +// } +// +// @objc func registerCustomFonts() { +// FontManager.shared.registerCustomFonts() +// } +//} diff --git a/ios/keyman/Keyman/Keyman/AppDelegate.swift b/ios/keyman/Keyman/Keyman/AppDelegate.swift index 34076ee254..7a5bf3b5b8 100644 --- a/ios/keyman/Keyman/Keyman/AppDelegate.swift +++ b/ios/keyman/Keyman/Keyman/AppDelegate.swift @@ -22,6 +22,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { + // We really should validate that it is a .kmp first... but the app doesn't yet + // process URL links, so it's fine for now. (Will change with QR code stuff.) + // .kmp package install, Keyman 10 onwards var destinationUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] destinationUrl.appendPathComponent("\(url.lastPathComponent).zip") @@ -80,13 +83,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return true } - func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool { - NotificationCenter.default.post(name: launchedFromUrlNotification, object: self, - userInfo: [urlKey: url] - ) - return true - } - func applicationDidEnterBackground(_ application: UIApplication) { _overlayWindow = nil FontManager.shared.unregisterCustomFonts() @@ -152,72 +148,19 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } public func promptAdHocInstall(_ kmp: KeymanPackage) { - _adhocDirectory = kmp.sourceFolder - let isKbd = kmp.isKeyboard() + let vc = PackageInstallViewController(for: kmp, completionHandler: { error in + if let err = error { + if let kmpError = err as? KMPError { + self.showKMPError(kmpError) + } + } else { + self.showSimpleAlert(title: "Success", message: "Installed successfully.") + } + }) - let vc = UIViewController() - vc.view.backgroundColor = .red - let wkWebView = WKWebView.init(frame: vc.view.frame) - wkWebView.backgroundColor = .white - vc.view.addSubview(wkWebView) - let cancelBtn = UIBarButtonItem(title: "Cancel", style: .plain, - target: self, - action: #selector(cancelAdHocBtnHandler)) - let installBtn = UIBarButtonItem(title: "Install", style: .plain, - target: self, - action: (isKbd ? #selector(installAdHocKeyboardBtnHandler) : - #selector(installAdHocLexicalModelBtnHandler)) ) - vc.navigationItem.leftBarButtonItem = cancelBtn - vc.navigationItem.rightBarButtonItem = installBtn let nvc = UINavigationController.init(rootViewController: vc) - self.window?.rootViewController?.present(nvc, animated: true, completion: { - wkWebView.loadHTMLString(kmp.infoHtml(), baseURL: nil) - }) - } - - @objc func installAdHocKeyboardBtnHandler() { - if let adhocDir = _adhocDirectory { - self.window?.rootViewController?.dismiss(animated: true, completion: { - do { - try Manager.shared.parseKbdKMP(adhocDir) - self.showSimpleAlert(title: "Success", message: "Installed successfully.") - } catch { - self.showKMPError(error as! KMPError) - } - - //this can fail gracefully and not show errors to users - do { - try FileManager.default.removeItem(at: adhocDir) - } catch { - log.error("unable to delete temp files") - } - }) - } - } - - @objc func installAdHocLexicalModelBtnHandler() { - if let adhocDir = _adhocDirectory { - self.window?.rootViewController?.dismiss(animated: true, completion: { - do { - try Manager.parseLMKMP(adhocDir) - self.showSimpleAlert(title: "Success", message: "Installed successfully.") - } catch { - self.showKMPError(error as! KMPError) - } - - //this can fail gracefully and not show errors to users - do { - try FileManager.default.removeItem(at: adhocDir) - } catch { - log.error("unable to delete temp files") - } - }) - } - } - - @objc func cancelAdHocBtnHandler() { - self.window?.rootViewController?.dismiss(animated: true, completion: nil) + self.window?.rootViewController?.present(nvc, animated: true, completion: nil) } @objc func registerCustomFonts() { From 5da632ce1a5d0ec72d0f5c646d29e9c01cb98faa Mon Sep 17 00:00:00 2001 From: jahorton Date: Wed, 18 Dec 2019 12:33:52 +0700 Subject: [PATCH 2/3] refactor(ios): Relocates AppDelegate KMP install methods --- .../ResourceFileManager.swift | 278 ++++++++---------- ios/keyman/Keyman/Keyman/AppDelegate.swift | 67 +---- .../Keyman/Keyman/MainViewController.swift | 4 +- 3 files changed, 132 insertions(+), 217 deletions(-) diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift index 15daeaba98..633441fda4 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift @@ -1,155 +1,129 @@ -//// -//// ResourceManager.swift -//// KeymanEngine -//// -//// Created by Joshua Horton on 12/18/19. -//// Copyright © 2019 SIL International. All rights reserved. -//// // -//import Foundation +// ResourceManager.swift +// KeymanEngine // -///** -// * This class stores common methods used for installing language resources, regardless of source. -// * -// * It also contains methods for general-purpose installation of language resources from .kmp files. -// */ -//public class ResourceFileManager { -// public static let shared = ResourceFileManager() +// Created by Joshua Horton on 12/18/19. +// Copyright © 2019 SIL International. All rights reserved. // -// fileprivate init() { -// } -// -// /** -// * Use this function to "install" external KMP files to within the Keyman app's alloted iOS file management domain. -// * Note that we don't request permissions to support opening/modifying files "in place," so we need to copy .kmps -// * before unzipping them. -// * -// * This implementation does not change how files are managed by the app; only where the file management code -// * is located. -// */ -// public func installFile(_ url: URL) { -// // Once selected, start the standard install process. -// log.info("Installing KMP at \(url)") -// -// // Step 1: Copy it to within the app's controlled space, making it a .zip in the process -// var destinationUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] -// destinationUrl.appendPathComponent("\(url.lastPathComponent).zip") -// -// do { -// let fileManager = FileManager.default -// -// // For now, we'll always allow overwriting. -// if fileManager.fileExists(atPath: destinationUrl.path) { -// try fileManager.removeItem(at: destinationUrl) -// } -// -// // Throws an error if the destination file already exists, and there's no -// // built-in override parameter. Hence, the previous if-block. -// try fileManager.copyItem(at: url, to: destinationUrl) -// installAdhocKeyboard(url: destinationUrl) -// return true -// } catch { -// showKMPError(KMPError.copyFiles) -// log.error(error) -// return -// } -// -// // Now, install it as if we'd just downloaded it. -// let resourceManager = ResourceDownloadManager.shared -// // Obviously, a BIG assumption here: that it's a lexical model, not a keyboard. -// // Pardon the proof of concept. -// guard let lexicalModels = resourceManager.installLexicalModelPackage(at: destinationUrl) else { -// log.info("Could not install KMP at \(url)") -// return -// } -// -// log.info("Attempt success!") -// } -// -// private func installAdhocKeyboard(url: URL) { -// let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] -// var destination = documentsDirectory -// destination.appendPathComponent("temp/\(url.lastPathComponent)") -// -// KeymanPackage.extract(fileUrl: url, destination: destination, complete: { kmp in -// if let kmp = kmp { -// self.promptAdHocInstall(kmp) -// } else { -// self.showKMPError(KMPError.invalidPackage) -// } -// }) -// } -// -// public func promptAdHocInstall(_ kmp: KeymanPackage) { -// _adhocDirectory = kmp.sourceFolder -// let isKbd = kmp.isKeyboard() -// -// let vc = UIViewController() -// vc.view.backgroundColor = .red -// let wkWebView = WKWebView.init(frame: vc.view.frame) -// wkWebView.backgroundColor = .white -// vc.view.addSubview(wkWebView) -// let cancelBtn = UIBarButtonItem(title: "Cancel", style: .plain, -// target: self, -// action: #selector(cancelAdHocBtnHandler)) -// let installBtn = UIBarButtonItem(title: "Install", style: .plain, -// target: self, -// action: (isKbd ? #selector(installAdHocKeyboardBtnHandler) : -// #selector(installAdHocLexicalModelBtnHandler)) ) -// vc.navigationItem.leftBarButtonItem = cancelBtn -// vc.navigationItem.rightBarButtonItem = installBtn -// let nvc = UINavigationController.init(rootViewController: vc) -// -// self.window?.rootViewController?.present(nvc, animated: true, completion: { -// wkWebView.loadHTMLString(kmp.infoHtml(), baseURL: nil) -// }) -// } -// -// @objc func installAdHocKeyboardBtnHandler() { -// if let adhocDir = _adhocDirectory { -// self.window?.rootViewController?.dismiss(animated: true, completion: { -// do { -// try Manager.shared.parseKbdKMP(adhocDir) -// self.showSimpleAlert(title: "Success", message: "Installed successfully.") -// } catch { -// self.showKMPError(error as! KMPError) -// } -// -// //this can fail gracefully and not show errors to users -// do { -// try FileManager.default.removeItem(at: adhocDir) -// } catch { -// log.error("unable to delete temp files") -// } -// }) -// } -// } -// -// @objc func installAdHocLexicalModelBtnHandler() { -// if let adhocDir = _adhocDirectory { -// self.window?.rootViewController?.dismiss(animated: true, completion: { -// do { -// try Manager.parseLMKMP(adhocDir) -// self.showSimpleAlert(title: "Success", message: "Installed successfully.") -// } catch { -// self.showKMPError(error as! KMPError) -// } -// -// //this can fail gracefully and not show errors to users -// do { -// try FileManager.default.removeItem(at: adhocDir) -// } catch { -// log.error("unable to delete temp files") -// } -// }) -// } -// } -// -// @objc func cancelAdHocBtnHandler() { -// self.window?.rootViewController?.dismiss(animated: true, completion: nil) -// } -// -// @objc func registerCustomFonts() { -// FontManager.shared.registerCustomFonts() -// } -//} + +import Foundation + +/** + * This class stores common methods used for installing language resources, regardless of source. + * + * It also contains methods for general-purpose installation of language resources from .kmp files. + */ +public class ResourceFileManager { + public static let shared = ResourceFileManager() + + fileprivate init() { + } + + /** + * Apple doesn't provide a method that performs copy-and-overwrite functionality. This function fills in that gap. + */ + private func copyWithOverwrite(from source: URL, to destination: URL) throws { + let fileManager = FileManager.default + + // For now, we'll always allow overwriting. + if fileManager.fileExists(atPath: destination.path) { + try fileManager.removeItem(at: destination) + } + + // Throws an error if the destination file already exists, and there's no + // built-in override parameter. Hence, the previous if-block. + try fileManager.copyItem(at: source, to: destination) + } + + /** + * Use this function to "import" a file from outside the app's designated file system area to a new location within, + * copying the original. It will be placed within the app's Documents folder. + * + * Returns the app-owned destination path, usable for subsequent file operations. + */ + public func importFile(_ url: URL) -> URL? { + var destinationUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] + destinationUrl.appendPathComponent(url.lastPathComponent) + + do { + try copyWithOverwrite(from: url, to: destinationUrl) + return destinationUrl + } catch { + log.error(error) + return nil + } + } + + /** + * Use this function to "install" external KMP files to within the Keyman app's alloted iOS file management domain. + * Note that we don't request permissions to support opening/modifying files "in place," so we need to copy .kmps + * before unzipping them. + * + * This implementation does not change how files are managed by the app; only where the file management code + * is located. + */ + @available(iOSApplicationExtension, unavailable) + public func installFile(_ url: URL) { + // Once selected, start the standard install process. + log.info("Installing KMP from \(url)") + + // Step 1: Copy it to a temporary location, making it a .zip in the process + var destinationUrl = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0] + destinationUrl.appendPathComponent("\(url.lastPathComponent).zip") + + do { + try copyWithOverwrite(from: url, to: destinationUrl) + installAdhocKeyboard(url: destinationUrl) + } catch { + showKMPError(KMPError.copyFiles) + log.error(error) + } + } + + @available(iOSApplicationExtension, unavailable) + private func installAdhocKeyboard(url: URL) { + let documentsDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0] + var destination = documentsDirectory + destination.appendPathComponent("temp/\(url.lastPathComponent)") + + KeymanPackage.extract(fileUrl: url, destination: destination, complete: { kmp in + if let kmp = kmp { + self.promptAdHocInstall(kmp) + } else { + self.showKMPError(KMPError.invalidPackage) + } + }) + } + + @available(iOSApplicationExtension, unavailable) + private func promptAdHocInstall(_ kmp: KeymanPackage) { + let vc = PackageInstallViewController(for: kmp, completionHandler: { error in + if let err = error { + if let kmpError = err as? KMPError { + self.showKMPError(kmpError) + } + } else { + self.showSimpleAlert(title: "Success", message: "Installed successfully.") + } + }) + + let nvc = UINavigationController.init(rootViewController: vc) + UIApplication.shared.keyWindow?.rootViewController?.present(nvc, animated: true, completion: nil) + } + + @available(iOSApplicationExtension, unavailable) + public func showKMPError(_ error: KMPError) { + showSimpleAlert(title: "Error", message: error.rawValue) + } + + @available(iOSApplicationExtension, unavailable) + public func showSimpleAlert(title: String, message: String) { + let alertController = UIAlertController(title: title, message: message, + preferredStyle: UIAlertController.Style.alert) + alertController.addAction(UIAlertAction(title: "OK", + style: UIAlertAction.Style.default, + handler: nil)) + + UIApplication.shared.keyWindow?.rootViewController?.present(alertController, animated: true, completion: nil) + } +} diff --git a/ios/keyman/Keyman/Keyman/AppDelegate.swift b/ios/keyman/Keyman/Keyman/AppDelegate.swift index 7a5bf3b5b8..136bdb1928 100644 --- a/ios/keyman/Keyman/Keyman/AppDelegate.swift +++ b/ios/keyman/Keyman/Keyman/AppDelegate.swift @@ -25,27 +25,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // We really should validate that it is a .kmp first... but the app doesn't yet // process URL links, so it's fine for now. (Will change with QR code stuff.) - // .kmp package install, Keyman 10 onwards - var destinationUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] - destinationUrl.appendPathComponent("\(url.lastPathComponent).zip") - do { - let fileManager = FileManager.default - - // For now, we'll always allow overwriting. - if fileManager.fileExists(atPath: destinationUrl.path) { - try fileManager.removeItem(at: destinationUrl) - } - - // Throws an error if the destination file already exists, and there's no - // built-in override parameter. Hence, the previous if-block. - try fileManager.copyItem(at: url, to: destinationUrl) - installAdhocKeyboard(url: destinationUrl) - return true - } catch { - showKMPError(KMPError.copyFiles) - log.error(error) + guard let destinationUrl = ResourceFileManager.shared.importFile(url) else { return false } + + ResourceFileManager.shared.installFile(destinationUrl) + return true } func application(_ application: UIApplication, @@ -119,50 +104,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return _overlayWindow! } - public func installAdhocKeyboard(url: URL) { - let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] - var destination = documentsDirectory - destination.appendPathComponent("temp/\(url.lastPathComponent)") - - KeymanPackage.extract(fileUrl: url, destination: destination, complete: { kmp in - if let kmp = kmp { - self.promptAdHocInstall(kmp) - } else { - self.showKMPError(KMPError.invalidPackage) - } - }) - } - - public func showKMPError(_ error: KMPError) { - showSimpleAlert(title: "Error", message: error.rawValue) - } - - public func showSimpleAlert(title: String, message: String) { - let alertController = UIAlertController(title: title, message: message, - preferredStyle: UIAlertController.Style.alert) - alertController.addAction(UIAlertAction(title: "OK", - style: UIAlertAction.Style.default, - handler: nil)) - - self.window?.rootViewController?.present(alertController, animated: true, completion: nil) - } - - public func promptAdHocInstall(_ kmp: KeymanPackage) { - let vc = PackageInstallViewController(for: kmp, completionHandler: { error in - if let err = error { - if let kmpError = err as? KMPError { - self.showKMPError(kmpError) - } - } else { - self.showSimpleAlert(title: "Success", message: "Installed successfully.") - } - }) - - let nvc = UINavigationController.init(rootViewController: vc) - - self.window?.rootViewController?.present(nvc, animated: true, completion: nil) - } - @objc func registerCustomFonts() { FontManager.shared.registerCustomFonts() } diff --git a/ios/keyman/Keyman/Keyman/MainViewController.swift b/ios/keyman/Keyman/Keyman/MainViewController.swift index 47572ab10d..8db8d1db5f 100644 --- a/ios/keyman/Keyman/Keyman/MainViewController.swift +++ b/ios/keyman/Keyman/Keyman/MainViewController.swift @@ -476,7 +476,7 @@ class MainViewController: UIViewController, TextViewDelegate, UIActionSheetDeleg if launchUrl != nil { perform(#selector(self.dismissActivityIndicator), with: nil, afterDelay: 1.0) let error = notification.error - appDelegate.showSimpleAlert(title: "Keyboard Download Error", message: error.localizedDescription) + ResourceFileManager.shared.showSimpleAlert(title: "Keyboard Download Error", message: error.localizedDescription) launchUrl = nil } } @@ -825,7 +825,7 @@ class MainViewController: UIViewController, TextViewDelegate, UIActionSheetDeleg if let urlString = params["url"] { // Download and set custom keyboard guard let url = URL(string: urlString) else { - appDelegate.showSimpleAlert(title: "Custom Keyboard", + ResourceFileManager.shared.showSimpleAlert(title: "Custom Keyboard", message: "The keyboard could not be installed: Invalid Url") launchUrl = nil return From 2097cf1fa72a27876acc45a6882f582e0bcfa521 Mon Sep 17 00:00:00 2001 From: jahorton Date: Thu, 19 Dec 2019 08:06:32 +0700 Subject: [PATCH 3/3] docs(ios): Fixed comments in ResourceFileManager.swift --- .../Classes/Resource Management/ResourceFileManager.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift index 633441fda4..193fc952d3 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift @@ -1,8 +1,8 @@ // -// ResourceManager.swift +// ResourceFileManager.swift // KeymanEngine // -// Created by Joshua Horton on 12/18/19. +// Created by Joshua Horton on December 18, 2019. // Copyright © 2019 SIL International. All rights reserved. //