Merge pull request #2460 from keymanapp/feat/ios/adhoc-install-refactor

refactor(ios): Adhoc installation code refactor/reorg
This commit is contained in:
Joshua Horton 2019-12-19 08:07:48 +07:00 • committed by GitHub
commit 692a94104f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 239 additions and 126 deletions

View file

@ -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 = "<group>"; };
CE2B1E4521B60E7C007D092E /* DeviceKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = DeviceKit.framework; path = ../../Carthage/Build/iOS/DeviceKit.framework; sourceTree = "<group>"; };
CE67D960228A6F190029F2B5 /* KeyboardCommandStructs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardCommandStructs.swift; sourceTree = "<group>"; };
CE71705723A9C14D00A924A1 /* ResourceFileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourceFileManager.swift; sourceTree = "<group>"; };
CE71705E23A9C97F00A924A1 /* PackageInstallViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackageInstallViewController.swift; sourceTree = "<group>"; };
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 = "<group>";
@ -738,6 +740,17 @@
name = KeymanWebView;
sourceTree = "<group>";
};
CE71705923A9C7D300A924A1 /* Resource Management */ = {
isa = PBXGroup;
children = (
CE1F67A22304EB3800FF6972 /* ResourceDownloadManager.swift */,
CE22DFB9230B94DB00A4551C /* ResourceDownloadQueue.swift */,
CE71705723A9C14D00A924A1 /* ResourceFileManager.swift */,
CE71705E23A9C97F00A924A1 /* PackageInstallViewController.swift */,
);
path = "Resource Management";
sourceTree = "<group>";
};
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 */,

View file

@ -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)
}
})
}
}

View file

@ -0,0 +1,129 @@
//
// ResourceFileManager.swift
// KeymanEngine
//
// Created by Joshua Horton on December 18, 2019.
// 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() {
}
/**
* 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)
}
}

View file

@ -22,27 +22,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ app: UIApplication, open url: URL,
options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
// .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
// 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.)
// 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,
@ -80,13 +68,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()
@ -123,103 +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) {
_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()
}

View file

@ -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