From 54c3933379eaeb94cc622b241196e473928bf3d0 Mon Sep 17 00:00:00 2001 From: jahorton Date: Fri, 6 Dec 2019 12:49:35 +0700 Subject: [PATCH 01/12] feat(ios): in-progress on Files integration --- .../KMEI/KeymanEngine/Classes/Manager.swift | 2 + .../Settings/SettingsViewController.swift | 30 ++++++++- ios/keyman/Keyman/Keyman-Info.plist | 2 +- .../Keyman/Keyman.xcodeproj/project.pbxproj | 4 ++ .../Keyman/Keyman/MainViewController.swift | 9 +++ .../Keyman/PackageBrowserViewController.swift | 67 +++++++++++++++++++ 6 files changed, 110 insertions(+), 4 deletions(-) create mode 100644 ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift b/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift index 89757bd9b0..d9672e6f7b 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift @@ -44,6 +44,8 @@ public class Manager: NSObject, UIGestureRecognizerDelegate { public static let shared = Manager() + public var fileBrowserLauncher: ((UINavigationController) -> Void)? = nil + /// Display the help bubble on first use. public var isKeymanHelpOn = true diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Settings/SettingsViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/Settings/SettingsViewController.swift index f7d207bb3c..d89d9cecc8 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Settings/SettingsViewController.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Settings/SettingsViewController.swift @@ -70,6 +70,16 @@ open class SettingsViewController: UITableViewController { "subtitle": "", "reuseid" : "showgetstarted" ]) + + // The iOS Files app is only available with 11.0+. + if #available(iOS 11.0, *) { + itemsArray.append([ + "title": "Install From File", + "subtitle": "Browse for .kmp files", + "reuseid" : "installfile" + ]) + } + _ = view } @@ -85,8 +95,7 @@ open class SettingsViewController: UITableViewController { } override open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - // #warning Incomplete implementation, return the number of rows - return 3 + return itemsArray.count } public func frameAtRightOfCell(cell cellFrame: CGRect, controlSize: CGSize) -> CGRect { @@ -149,6 +158,8 @@ open class SettingsViewController: UITableViewController { showAgainSwitch.rightAnchor.constraint(equalTo: cell.layoutMarginsGuide.rightAnchor).isActive = true showAgainSwitch.centerYAnchor.constraint(equalTo: cell.layoutMarginsGuide.centerYAnchor).isActive = true } + case "installfile": + cell.accessoryType = .disclosureIndicator default: log.error("unknown cellIdentifier(\"\(cellIdentifier ?? "EMPTY")\")") cell.accessoryType = .none @@ -188,6 +199,8 @@ open class SettingsViewController: UITableViewController { if indexPath.row == 0 { cell.accessoryType = .disclosureIndicator + } else if indexPath.row == 3 { + cell.accessoryType = .disclosureIndicator } else { cell.textLabel?.isEnabled = true cell.detailTextLabel?.isEnabled = false @@ -208,7 +221,18 @@ open class SettingsViewController: UITableViewController { private func performAction(for indexPath: IndexPath) { switch indexPath.section { case 0: - showLanguages() + switch indexPath.row { + case 0: + showLanguages() + case 3: + if let block = Manager.shared.fileBrowserLauncher { + block(navigationController!) + } else { + log.info("Listener for framework signal to launch file browser is missing") + } + default: + break + } default: break } diff --git a/ios/keyman/Keyman/Keyman-Info.plist b/ios/keyman/Keyman/Keyman-Info.plist index 2263aad0d4..29f1c86509 100644 --- a/ios/keyman/Keyman/Keyman-Info.plist +++ b/ios/keyman/Keyman/Keyman-Info.plist @@ -80,7 +80,7 @@ UIInterfaceOrientationLandscapeRight UISupportsDocumentBrowser - + UTExportedTypeDeclarations diff --git a/ios/keyman/Keyman/Keyman.xcodeproj/project.pbxproj b/ios/keyman/Keyman/Keyman.xcodeproj/project.pbxproj index 8e81b66515..9916a85700 100644 --- a/ios/keyman/Keyman/Keyman.xcodeproj/project.pbxproj +++ b/ios/keyman/Keyman/Keyman.xcodeproj/project.pbxproj @@ -135,6 +135,7 @@ CE6138011FB99538009D0EF2 /* KeymanEngine.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CE6137FF1FB99538009D0EF2 /* KeymanEngine.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; CE6138021FB999C8009D0EF2 /* KeymanEngine.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE6138031FB999C8009D0EF2 /* KeymanEngine.framework */; }; CE7C1AE2236925D800100C2C /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CE7C1AE1236925D800100C2C /* LaunchScreen.storyboard */; }; + CE7FF1F0239A0293007859D9 /* PackageBrowserViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7FF1EF239A0293007859D9 /* PackageBrowserViewController.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -298,6 +299,7 @@ CE6137FF1FB99538009D0EF2 /* KeymanEngine.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = KeymanEngine.framework; sourceTree = BUILT_PRODUCTS_DIR; }; CE6138031FB999C8009D0EF2 /* KeymanEngine.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = KeymanEngine.framework; sourceTree = BUILT_PRODUCTS_DIR; }; CE7C1AE1236925D800100C2C /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; + CE7FF1EF239A0293007859D9 /* PackageBrowserViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackageBrowserViewController.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -521,6 +523,7 @@ 9845A7C71A439A9200544E2E /* SetUpViewController */, 98ABADBA176935E400B62590 /* Supporting Files */, C0FF769D1F5D4ECB00BD23C3 /* ActivityItemProvider.swift */, + CE7FF1EF239A0293007859D9 /* PackageBrowserViewController.swift */, C0FF769C1F5D4ECA00BD23C3 /* Keyman-Bridging-Header.h */, C0E943F71F61234C00E7D98C /* UIImage+Helpers.swift */, C0E943F91F6124E100E7D98C /* AppDelegate.swift */, @@ -924,6 +927,7 @@ C055F0B11F60E8D400140735 /* GetStartedViewController.swift in Sources */, C055F0B51F610FB200140735 /* DropDownListView.swift in Sources */, C059FCC01FD927EF00BD1A64 /* Log.swift in Sources */, + CE7FF1F0239A0293007859D9 /* PackageBrowserViewController.swift in Sources */, C0E943F81F61234C00E7D98C /* UIImage+Helpers.swift in Sources */, C0E943FE1F61377900E7D98C /* MainViewController.swift in Sources */, 162E2C9920926C8600F40769 /* UIView+Extensions.swift in Sources */, diff --git a/ios/keyman/Keyman/Keyman/MainViewController.swift b/ios/keyman/Keyman/Keyman/MainViewController.swift index 47572ab10d..4929ed8837 100644 --- a/ios/keyman/Keyman/Keyman/MainViewController.swift +++ b/ios/keyman/Keyman/Keyman/MainViewController.swift @@ -122,6 +122,15 @@ class MainViewController: UIViewController, TextViewDelegate, UIActionSheetDeleg observer: self, function: MainViewController.keyboardRemoved) + // Unfortunately, it's the main app with the file definitions. + // We have to gerry-rig this so that the framework-based SettingsViewController + // can launch the app-based DocumentViewController. + if #available(iOS 11.0, *) { + Manager.shared.fileBrowserLauncher = { navController in + let vc = PackageBrowserViewController() + navController.pushViewController(vc, animated: true) + } + } } required init?(coder aDecoder: NSCoder) { diff --git a/ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift b/ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift new file mode 100644 index 0000000000..9a5f742463 --- /dev/null +++ b/ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift @@ -0,0 +1,67 @@ +// +// PackageBrowserViewController.swift +// Keyman +// +// Created by Joshua Horton on 12/6/19. +// Copyright © 2019 SIL International. All rights reserved. +// + +import UIKit +import SwiftUI + +@available(iOS 11.0, *) +class PackageBrowserViewController: UIDocumentBrowserViewController, UIDocumentBrowserViewControllerDelegate { + + override func viewDidLoad() { + super.viewDidLoad() + + delegate = self + + allowsDocumentCreation = false + allowsPickingMultipleItems = false + + // Update the style of the UIDocumentBrowserViewController + // browserUserInterfaceStyle = .dark + // view.tintColor = .white + + // Specify the allowed content types of your application via the Info.plist. + + // Do any additional setup after loading the view. + } + + // MARK: UIDocumentBrowserViewControllerDelegate + + func documentBrowser(_ controller: UIDocumentBrowserViewController, didPickDocumentsAt documentURLs: [URL]) { + guard let sourceURL = documentURLs.first else { return } + + // Present the Document View Controller for the first document that was picked. + // If you support picking multiple items, make sure you handle them all. + presentDocument(at: sourceURL) + } + + func documentBrowser(_ controller: UIDocumentBrowserViewController, + didImportDocumentAt sourceURL: URL, + toDestinationURL destinationURL: URL) { + // Present the Document View Controller for the new newly created document + presentDocument(at: destinationURL) + } + + func documentBrowser(_ controller: UIDocumentBrowserViewController, + failedToImportDocumentAt documentURL: URL, + error: Error?) { + // Make sure to handle the failed import appropriately, e.g., by presenting an error message to the user. + } + + // MARK: Document Presentation + + func presentDocument(at documentURL: URL) { + // Once selected, start the standard install process. + log.info("TODO: install KMP at \(documentURL)") + + // Re-use the function called by iOS for file-sharing. + let appDelegate = UIApplication.shared.delegate as! AppDelegate + let success = appDelegate.application(UIApplication.shared, open: documentURL) + + log.info("Attempt success: \(success)") + } +} From 518664930f540acd335e42a2b5e7c8ab4519302d Mon Sep 17 00:00:00 2001 From: jahorton Date: Tue, 17 Dec 2019 10:36:10 +0700 Subject: [PATCH 02/12] feat(ios): Basic link b/t doc browser and lex model installation --- .../ResourceDownloadManager.swift | 4 ++ .../Keyman/PackageBrowserViewController.swift | 49 ++++++++++++++++--- 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/ios/engine/KMEI/KeymanEngine/Classes/KeyboardRepository/ResourceDownloadManager.swift b/ios/engine/KMEI/KeymanEngine/Classes/KeyboardRepository/ResourceDownloadManager.swift index bdf91122f8..cefc8f1649 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/KeyboardRepository/ResourceDownloadManager.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/KeyboardRepository/ResourceDownloadManager.swift @@ -581,4 +581,8 @@ public class ResourceDownloadManager { return updateQueue } + + public func installLexicalModelPackage(at packageURL: URL) -> InstallableLexicalModel? { + return downloader.installLexicalModelPackage(downloadedPackageFile: packageURL) + } } diff --git a/ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift b/ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift index 9a5f742463..83a4189516 100644 --- a/ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift +++ b/ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift @@ -8,6 +8,7 @@ import UIKit import SwiftUI +import KeymanEngine @available(iOS 11.0, *) class PackageBrowserViewController: UIDocumentBrowserViewController, UIDocumentBrowserViewControllerDelegate { @@ -54,14 +55,50 @@ class PackageBrowserViewController: UIDocumentBrowserViewController, UIDocumentB // MARK: Document Presentation - func presentDocument(at documentURL: URL) { + func presentDocument(at documentUrl: URL) { // Once selected, start the standard install process. - log.info("TODO: install KMP at \(documentURL)") + log.info("Installing KMP at \(documentUrl)") - // Re-use the function called by iOS for file-sharing. - let appDelegate = UIApplication.shared.delegate as! AppDelegate - let success = appDelegate.application(UIApplication.shared, open: documentURL) + // 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("\(documentUrl.lastPathComponent).zip") - log.info("Attempt success: \(success)") + let fileManager = FileManager.default + do { + if fileManager.fileExists(atPath: destinationUrl.path) { + try fileManager.removeItem(at: destinationUrl) + } + try fileManager.copyItem(at: documentUrl, to: destinationUrl) + } catch { + showKMPError(KMPError.copyFiles) + log.error(error) + return + } + + // Now, install it as if we'd just downloaded it. + let resourceManager = ResourceDownloadManager.shared + guard let lexicalModels = resourceManager.installLexicalModelPackage(at: destinationUrl) else { + log.info("Could not install KMP at \(documentUrl)") + return + } + + log.info("Attempt success!") + } + + // The following are raw copies from AppDelegate. Not great, but it's a useful + // start while developing. + 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)) + + // This part is tweaked (in comparison to the AppDelegate version). + self.navigationController?.present(alertController, animated: true, completion: nil) } } From 75b6827a2744f11820ddb3d1bb2c15b42275ff10 Mon Sep 17 00:00:00 2001 From: jahorton Date: Tue, 17 Dec 2019 10:37:23 +0700 Subject: [PATCH 03/12] docs(ios): Comment on limitations of prior commit --- ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift b/ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift index 83a4189516..4a5dff72eb 100644 --- a/ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift +++ b/ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift @@ -77,6 +77,8 @@ class PackageBrowserViewController: UIDocumentBrowserViewController, UIDocumentB // 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 \(documentUrl)") return From 2d93ae241cae82382500350e5c33f4a8f14e1977 Mon Sep 17 00:00:00 2001 From: jahorton Date: Fri, 6 Dec 2019 12:49:35 +0700 Subject: [PATCH 04/12] feat(ios): in-progress on Files integration --- .../KMEI/KeymanEngine/Classes/Manager.swift | 2 + .../Settings/SettingsViewController.swift | 30 ++++++++- ios/keyman/Keyman/Keyman-Info.plist | 2 +- .../Keyman/Keyman.xcodeproj/project.pbxproj | 4 ++ .../Keyman/Keyman/MainViewController.swift | 9 +++ .../Keyman/PackageBrowserViewController.swift | 67 +++++++++++++++++++ 6 files changed, 110 insertions(+), 4 deletions(-) create mode 100644 ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift b/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift index 89757bd9b0..d9672e6f7b 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift @@ -44,6 +44,8 @@ public class Manager: NSObject, UIGestureRecognizerDelegate { public static let shared = Manager() + public var fileBrowserLauncher: ((UINavigationController) -> Void)? = nil + /// Display the help bubble on first use. public var isKeymanHelpOn = true diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Settings/SettingsViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/Settings/SettingsViewController.swift index f7d207bb3c..d89d9cecc8 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Settings/SettingsViewController.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Settings/SettingsViewController.swift @@ -70,6 +70,16 @@ open class SettingsViewController: UITableViewController { "subtitle": "", "reuseid" : "showgetstarted" ]) + + // The iOS Files app is only available with 11.0+. + if #available(iOS 11.0, *) { + itemsArray.append([ + "title": "Install From File", + "subtitle": "Browse for .kmp files", + "reuseid" : "installfile" + ]) + } + _ = view } @@ -85,8 +95,7 @@ open class SettingsViewController: UITableViewController { } override open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - // #warning Incomplete implementation, return the number of rows - return 3 + return itemsArray.count } public func frameAtRightOfCell(cell cellFrame: CGRect, controlSize: CGSize) -> CGRect { @@ -149,6 +158,8 @@ open class SettingsViewController: UITableViewController { showAgainSwitch.rightAnchor.constraint(equalTo: cell.layoutMarginsGuide.rightAnchor).isActive = true showAgainSwitch.centerYAnchor.constraint(equalTo: cell.layoutMarginsGuide.centerYAnchor).isActive = true } + case "installfile": + cell.accessoryType = .disclosureIndicator default: log.error("unknown cellIdentifier(\"\(cellIdentifier ?? "EMPTY")\")") cell.accessoryType = .none @@ -188,6 +199,8 @@ open class SettingsViewController: UITableViewController { if indexPath.row == 0 { cell.accessoryType = .disclosureIndicator + } else if indexPath.row == 3 { + cell.accessoryType = .disclosureIndicator } else { cell.textLabel?.isEnabled = true cell.detailTextLabel?.isEnabled = false @@ -208,7 +221,18 @@ open class SettingsViewController: UITableViewController { private func performAction(for indexPath: IndexPath) { switch indexPath.section { case 0: - showLanguages() + switch indexPath.row { + case 0: + showLanguages() + case 3: + if let block = Manager.shared.fileBrowserLauncher { + block(navigationController!) + } else { + log.info("Listener for framework signal to launch file browser is missing") + } + default: + break + } default: break } diff --git a/ios/keyman/Keyman/Keyman-Info.plist b/ios/keyman/Keyman/Keyman-Info.plist index 2263aad0d4..29f1c86509 100644 --- a/ios/keyman/Keyman/Keyman-Info.plist +++ b/ios/keyman/Keyman/Keyman-Info.plist @@ -80,7 +80,7 @@ UIInterfaceOrientationLandscapeRight UISupportsDocumentBrowser - + UTExportedTypeDeclarations diff --git a/ios/keyman/Keyman/Keyman.xcodeproj/project.pbxproj b/ios/keyman/Keyman/Keyman.xcodeproj/project.pbxproj index 8e81b66515..9916a85700 100644 --- a/ios/keyman/Keyman/Keyman.xcodeproj/project.pbxproj +++ b/ios/keyman/Keyman/Keyman.xcodeproj/project.pbxproj @@ -135,6 +135,7 @@ CE6138011FB99538009D0EF2 /* KeymanEngine.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CE6137FF1FB99538009D0EF2 /* KeymanEngine.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; CE6138021FB999C8009D0EF2 /* KeymanEngine.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE6138031FB999C8009D0EF2 /* KeymanEngine.framework */; }; CE7C1AE2236925D800100C2C /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CE7C1AE1236925D800100C2C /* LaunchScreen.storyboard */; }; + CE7FF1F0239A0293007859D9 /* PackageBrowserViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7FF1EF239A0293007859D9 /* PackageBrowserViewController.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -298,6 +299,7 @@ CE6137FF1FB99538009D0EF2 /* KeymanEngine.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = KeymanEngine.framework; sourceTree = BUILT_PRODUCTS_DIR; }; CE6138031FB999C8009D0EF2 /* KeymanEngine.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = KeymanEngine.framework; sourceTree = BUILT_PRODUCTS_DIR; }; CE7C1AE1236925D800100C2C /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; + CE7FF1EF239A0293007859D9 /* PackageBrowserViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackageBrowserViewController.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -521,6 +523,7 @@ 9845A7C71A439A9200544E2E /* SetUpViewController */, 98ABADBA176935E400B62590 /* Supporting Files */, C0FF769D1F5D4ECB00BD23C3 /* ActivityItemProvider.swift */, + CE7FF1EF239A0293007859D9 /* PackageBrowserViewController.swift */, C0FF769C1F5D4ECA00BD23C3 /* Keyman-Bridging-Header.h */, C0E943F71F61234C00E7D98C /* UIImage+Helpers.swift */, C0E943F91F6124E100E7D98C /* AppDelegate.swift */, @@ -924,6 +927,7 @@ C055F0B11F60E8D400140735 /* GetStartedViewController.swift in Sources */, C055F0B51F610FB200140735 /* DropDownListView.swift in Sources */, C059FCC01FD927EF00BD1A64 /* Log.swift in Sources */, + CE7FF1F0239A0293007859D9 /* PackageBrowserViewController.swift in Sources */, C0E943F81F61234C00E7D98C /* UIImage+Helpers.swift in Sources */, C0E943FE1F61377900E7D98C /* MainViewController.swift in Sources */, 162E2C9920926C8600F40769 /* UIView+Extensions.swift in Sources */, diff --git a/ios/keyman/Keyman/Keyman/MainViewController.swift b/ios/keyman/Keyman/Keyman/MainViewController.swift index f0bab8247a..bb482ab41e 100644 --- a/ios/keyman/Keyman/Keyman/MainViewController.swift +++ b/ios/keyman/Keyman/Keyman/MainViewController.swift @@ -122,6 +122,15 @@ class MainViewController: UIViewController, TextViewDelegate, UIActionSheetDeleg observer: self, function: MainViewController.keyboardRemoved) + // Unfortunately, it's the main app with the file definitions. + // We have to gerry-rig this so that the framework-based SettingsViewController + // can launch the app-based DocumentViewController. + if #available(iOS 11.0, *) { + Manager.shared.fileBrowserLauncher = { navController in + let vc = PackageBrowserViewController() + navController.pushViewController(vc, animated: true) + } + } } required init?(coder aDecoder: NSCoder) { diff --git a/ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift b/ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift new file mode 100644 index 0000000000..9a5f742463 --- /dev/null +++ b/ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift @@ -0,0 +1,67 @@ +// +// PackageBrowserViewController.swift +// Keyman +// +// Created by Joshua Horton on 12/6/19. +// Copyright © 2019 SIL International. All rights reserved. +// + +import UIKit +import SwiftUI + +@available(iOS 11.0, *) +class PackageBrowserViewController: UIDocumentBrowserViewController, UIDocumentBrowserViewControllerDelegate { + + override func viewDidLoad() { + super.viewDidLoad() + + delegate = self + + allowsDocumentCreation = false + allowsPickingMultipleItems = false + + // Update the style of the UIDocumentBrowserViewController + // browserUserInterfaceStyle = .dark + // view.tintColor = .white + + // Specify the allowed content types of your application via the Info.plist. + + // Do any additional setup after loading the view. + } + + // MARK: UIDocumentBrowserViewControllerDelegate + + func documentBrowser(_ controller: UIDocumentBrowserViewController, didPickDocumentsAt documentURLs: [URL]) { + guard let sourceURL = documentURLs.first else { return } + + // Present the Document View Controller for the first document that was picked. + // If you support picking multiple items, make sure you handle them all. + presentDocument(at: sourceURL) + } + + func documentBrowser(_ controller: UIDocumentBrowserViewController, + didImportDocumentAt sourceURL: URL, + toDestinationURL destinationURL: URL) { + // Present the Document View Controller for the new newly created document + presentDocument(at: destinationURL) + } + + func documentBrowser(_ controller: UIDocumentBrowserViewController, + failedToImportDocumentAt documentURL: URL, + error: Error?) { + // Make sure to handle the failed import appropriately, e.g., by presenting an error message to the user. + } + + // MARK: Document Presentation + + func presentDocument(at documentURL: URL) { + // Once selected, start the standard install process. + log.info("TODO: install KMP at \(documentURL)") + + // Re-use the function called by iOS for file-sharing. + let appDelegate = UIApplication.shared.delegate as! AppDelegate + let success = appDelegate.application(UIApplication.shared, open: documentURL) + + log.info("Attempt success: \(success)") + } +} From 9b952a14fe6d35268268be736dd556c71215528b Mon Sep 17 00:00:00 2001 From: jahorton Date: Tue, 17 Dec 2019 10:36:10 +0700 Subject: [PATCH 05/12] feat(ios): Basic link b/t doc browser and lex model installation --- .../ResourceDownloadManager.swift | 4 ++ .../Keyman/PackageBrowserViewController.swift | 49 ++++++++++++++++--- 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceDownloadManager.swift b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceDownloadManager.swift index bdf91122f8..cefc8f1649 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceDownloadManager.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceDownloadManager.swift @@ -581,4 +581,8 @@ public class ResourceDownloadManager { return updateQueue } + + public func installLexicalModelPackage(at packageURL: URL) -> InstallableLexicalModel? { + return downloader.installLexicalModelPackage(downloadedPackageFile: packageURL) + } } diff --git a/ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift b/ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift index 9a5f742463..83a4189516 100644 --- a/ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift +++ b/ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift @@ -8,6 +8,7 @@ import UIKit import SwiftUI +import KeymanEngine @available(iOS 11.0, *) class PackageBrowserViewController: UIDocumentBrowserViewController, UIDocumentBrowserViewControllerDelegate { @@ -54,14 +55,50 @@ class PackageBrowserViewController: UIDocumentBrowserViewController, UIDocumentB // MARK: Document Presentation - func presentDocument(at documentURL: URL) { + func presentDocument(at documentUrl: URL) { // Once selected, start the standard install process. - log.info("TODO: install KMP at \(documentURL)") + log.info("Installing KMP at \(documentUrl)") - // Re-use the function called by iOS for file-sharing. - let appDelegate = UIApplication.shared.delegate as! AppDelegate - let success = appDelegate.application(UIApplication.shared, open: documentURL) + // 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("\(documentUrl.lastPathComponent).zip") - log.info("Attempt success: \(success)") + let fileManager = FileManager.default + do { + if fileManager.fileExists(atPath: destinationUrl.path) { + try fileManager.removeItem(at: destinationUrl) + } + try fileManager.copyItem(at: documentUrl, to: destinationUrl) + } catch { + showKMPError(KMPError.copyFiles) + log.error(error) + return + } + + // Now, install it as if we'd just downloaded it. + let resourceManager = ResourceDownloadManager.shared + guard let lexicalModels = resourceManager.installLexicalModelPackage(at: destinationUrl) else { + log.info("Could not install KMP at \(documentUrl)") + return + } + + log.info("Attempt success!") + } + + // The following are raw copies from AppDelegate. Not great, but it's a useful + // start while developing. + 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)) + + // This part is tweaked (in comparison to the AppDelegate version). + self.navigationController?.present(alertController, animated: true, completion: nil) } } From 3165b07b04bf34c28d9fb91f8e734eb05179913a Mon Sep 17 00:00:00 2001 From: jahorton Date: Tue, 17 Dec 2019 10:37:23 +0700 Subject: [PATCH 06/12] docs(ios): Comment on limitations of prior commit --- ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift b/ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift index 83a4189516..4a5dff72eb 100644 --- a/ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift +++ b/ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift @@ -77,6 +77,8 @@ class PackageBrowserViewController: UIDocumentBrowserViewController, UIDocumentB // 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 \(documentUrl)") return From c5e762eee872ba535bfb49ec31b07ec16b5b0403 Mon Sep 17 00:00:00 2001 From: jahorton Date: Thu, 19 Dec 2019 09:56:01 +0700 Subject: [PATCH 07/12] feat(ios): Fixes #2356, integrating file browser with kmp installation --- .../Keyman/PackageBrowserViewController.swift | 62 +++++-------------- 1 file changed, 15 insertions(+), 47 deletions(-) diff --git a/ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift b/ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift index 4a5dff72eb..eea6f3aea3 100644 --- a/ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift +++ b/ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift @@ -37,14 +37,14 @@ class PackageBrowserViewController: UIDocumentBrowserViewController, UIDocumentB // Present the Document View Controller for the first document that was picked. // If you support picking multiple items, make sure you handle them all. - presentDocument(at: sourceURL) + doInstall(of: sourceURL) } func documentBrowser(_ controller: UIDocumentBrowserViewController, didImportDocumentAt sourceURL: URL, toDestinationURL destinationURL: URL) { // Present the Document View Controller for the new newly created document - presentDocument(at: destinationURL) + doInstall(of: destinationURL) } func documentBrowser(_ controller: UIDocumentBrowserViewController, @@ -55,52 +55,20 @@ class PackageBrowserViewController: UIDocumentBrowserViewController, UIDocumentB // MARK: Document Presentation - func presentDocument(at documentUrl: URL) { - // Once selected, start the standard install process. - log.info("Installing KMP at \(documentUrl)") + func doInstall(of url: URL) { + // Once selected, start the standard install process. + let rfm = ResourceFileManager.shared - // 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("\(documentUrl.lastPathComponent).zip") + guard let destinationUrl = rfm.importFile(url) else { + return + } - let fileManager = FileManager.default - do { - if fileManager.fileExists(atPath: destinationUrl.path) { - try fileManager.removeItem(at: destinationUrl) - } - try fileManager.copyItem(at: documentUrl, to: destinationUrl) - } 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 \(documentUrl)") - return - } - - log.info("Attempt success!") - } - - // The following are raw copies from AppDelegate. Not great, but it's a useful - // start while developing. - 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)) - - // This part is tweaked (in comparison to the AppDelegate version). - self.navigationController?.present(alertController, animated: true, completion: nil) + rfm.prepareKMPInstall(from: destinationUrl, + alertHost: self, + completionHandler: { package in + // We choose to prompt the user for comfirmation, rather + // than automatically installing the package. + rfm.promptPackageInstall(of: package, in: self) + }) } } From b754bbc5889ba0da5d02f5f1f930560c368cfa37 Mon Sep 17 00:00:00 2001 From: jahorton Date: Thu, 19 Dec 2019 10:18:22 +0700 Subject: [PATCH 08/12] feat(ios): autodismissal of doc browser on install success --- .../Resource Management/ResourceFileManager.swift | 14 ++++++++++---- .../Keyman/PackageBrowserViewController.swift | 8 +++++++- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift index d6bc2820d2..58d3c9a9df 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift @@ -103,7 +103,9 @@ public class ResourceFileManager { }) } - public func promptPackageInstall(of package: KeymanPackage, in rootVC: UIViewController) { + public func promptPackageInstall(of package: KeymanPackage, + in rootVC: UIViewController, + successHandler: ((KeymanPackage) -> Void)? = nil) { let vc = PackageInstallViewController(for: package, completionHandler: { error in if let err = error { if let kmpError = err as? KMPError { @@ -111,7 +113,9 @@ public class ResourceFileManager { rootVC.present(alert, animated: true, completion: nil) } } else { - let alert = self.buildSimpleAlert(title: "Success", message: "Installed successfully.") + let alert = self.buildSimpleAlert(title: "Success", message: "Installed successfully.", completionHandler: { + successHandler?(package) + }) rootVC.present(alert, animated: true, completion: nil) } }) @@ -124,12 +128,14 @@ public class ResourceFileManager { return buildSimpleAlert(title: "Error", message: error.rawValue) } - public func buildSimpleAlert(title: String, message: String) -> UIAlertController { + public func buildSimpleAlert(title: String, message: String, completionHandler: (() -> Void)? = nil ) -> UIAlertController { let alertController = UIAlertController(title: title, message: message, preferredStyle: UIAlertController.Style.alert) alertController.addAction(UIAlertAction(title: "OK", style: UIAlertAction.Style.default, - handler: nil)) + handler: { _ in + completionHandler?() + })) //UIApplication.shared.keyWindow?.rootViewController?.present(alertController, animated: true, completion: nil) return alertController diff --git a/ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift b/ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift index eea6f3aea3..48bbabb1c6 100644 --- a/ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift +++ b/ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift @@ -68,7 +68,13 @@ class PackageBrowserViewController: UIDocumentBrowserViewController, UIDocumentB completionHandler: { package in // We choose to prompt the user for comfirmation, rather // than automatically installing the package. - rfm.promptPackageInstall(of: package, in: self) + rfm.promptPackageInstall(of: package, in: self, successHandler: { _ in + // Auto-dismiss the document browser upon successful KMP install. + // It's likely quite rare that someone would want to install 2+ at once. + // + // Problem: the dismissal isn't 'smooth' yet - it aborts straight to the main screen. + self.dismiss(animated: true, completion: nil) + }) }) } } From 035cb106af4fe11056c01c9cddc38e41874bb60a Mon Sep 17 00:00:00 2001 From: jahorton Date: Thu, 19 Dec 2019 10:20:53 +0700 Subject: [PATCH 09/12] fix(ios): checks that kmps aren't already imported --- .../Classes/Resource Management/ResourceFileManager.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift index 58d3c9a9df..5d91bd6431 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift @@ -45,6 +45,12 @@ public class ResourceFileManager { var destinationUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] destinationUrl.appendPathComponent(url.lastPathComponent) + // Since it's possible to request an install from a KMP in our owned document space, + // we need to check that it's not already in place where we want it. + if url == destinationUrl { + return url + } + do { try copyWithOverwrite(from: url, to: destinationUrl) return destinationUrl From 2b0daa3d2d5ed1054868c70dcd8b47bc72c6d9ca Mon Sep 17 00:00:00 2001 From: jahorton Date: Thu, 19 Dec 2019 10:37:33 +0700 Subject: [PATCH 10/12] docs(ios): adds file browser feat note to history.md --- ios/history.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ios/history.md b/ios/history.md index 74306c970a..2f0ff00392 100644 --- a/ios/history.md +++ b/ios/history.md @@ -2,6 +2,7 @@ ## 13.0 alpha * Start version 13.0 +* Adds file browsing for installable KMPs and makes KMPs for resources installed this way available to the Files app (#2457) * Testing for upcoming patch to stable: * Fixes for deprecated code, improving maintainability (#2282) From d5b38d12c0191a4d605ce378562dfe23b045b4e7 Mon Sep 17 00:00:00 2001 From: jahorton Date: Wed, 8 Jan 2020 09:42:44 +0700 Subject: [PATCH 11/12] fix(ios): better auto-dismiss from doc browser on install success --- ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift b/ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift index 48bbabb1c6..fcde824287 100644 --- a/ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift +++ b/ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift @@ -71,9 +71,7 @@ class PackageBrowserViewController: UIDocumentBrowserViewController, UIDocumentB rfm.promptPackageInstall(of: package, in: self, successHandler: { _ in // Auto-dismiss the document browser upon successful KMP install. // It's likely quite rare that someone would want to install 2+ at once. - // - // Problem: the dismissal isn't 'smooth' yet - it aborts straight to the main screen. - self.dismiss(animated: true, completion: nil) + self.navigationController?.popViewController(animated: true) }) }) } From 7644f0d8e6ad843bc26af07f90a16549cc7daacb Mon Sep 17 00:00:00 2001 From: jahorton Date: Fri, 10 Jan 2020 08:50:12 +0700 Subject: [PATCH 12/12] change(ios): whitespace (noted in review comment) --- ios/keyman/Keyman/Keyman/MainViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/keyman/Keyman/Keyman/MainViewController.swift b/ios/keyman/Keyman/Keyman/MainViewController.swift index bb482ab41e..316000d6ad 100644 --- a/ios/keyman/Keyman/Keyman/MainViewController.swift +++ b/ios/keyman/Keyman/Keyman/MainViewController.swift @@ -127,7 +127,7 @@ class MainViewController: UIViewController, TextViewDelegate, UIActionSheetDeleg // can launch the app-based DocumentViewController. if #available(iOS 11.0, *) { Manager.shared.fileBrowserLauncher = { navController in - let vc = PackageBrowserViewController() + let vc = PackageBrowserViewController() navController.pushViewController(vc, animated: true) } }