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