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/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/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift
index d6bc2820d2..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
@@ -103,7 +109,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 +119,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 +134,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/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/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)
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..316000d6ad 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..fcde824287
--- /dev/null
+++ b/ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift
@@ -0,0 +1,78 @@
+//
+// PackageBrowserViewController.swift
+// Keyman
+//
+// Created by Joshua Horton on 12/6/19.
+// Copyright © 2019 SIL International. All rights reserved.
+//
+
+import UIKit
+import SwiftUI
+import KeymanEngine
+
+@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.
+ doInstall(of: sourceURL)
+ }
+
+ func documentBrowser(_ controller: UIDocumentBrowserViewController,
+ didImportDocumentAt sourceURL: URL,
+ toDestinationURL destinationURL: URL) {
+ // Present the Document View Controller for the new newly created document
+ doInstall(of: 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 doInstall(of url: URL) {
+ // Once selected, start the standard install process.
+ let rfm = ResourceFileManager.shared
+
+ guard let destinationUrl = rfm.importFile(url) else {
+ return
+ }
+
+ 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, 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.
+ self.navigationController?.popViewController(animated: true)
+ })
+ })
+ }
+}