From f495d901646e255ee5249004d77116920c30153c Mon Sep 17 00:00:00 2001 From: jahorton Date: Mon, 27 Jul 2020 10:07:03 +0700 Subject: [PATCH 1/7] refactor(ios/engine): splits universal link parse to own class --- .../KeymanEngine.xcodeproj/project.pbxproj | 8 ++++ .../KeyboardSearchViewController.swift | 37 +++------------ .../KeymanEngine/Classes/UniversalLinks.swift | 41 +++++++++++++++++ .../KeyboardSearchTests.swift | 38 +-------------- .../UniversalLinkTests.swift | 46 +++++++++++++++++++ 5 files changed, 103 insertions(+), 67 deletions(-) create mode 100644 ios/engine/KMEI/KeymanEngine/Classes/UniversalLinks.swift create mode 100644 ios/engine/KMEI/KeymanEngineTests/UniversalLinkTests.swift diff --git a/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj b/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj index af3372d603..3f043a9fe3 100644 --- a/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj +++ b/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj @@ -210,6 +210,8 @@ CEA1486C2407808F00C6ECD2 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = CEA1486F2407808F00C6ECD2 /* Localizable.strings */; }; CEA1486D2407808F00C6ECD2 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = CEA1486F2407808F00C6ECD2 /* Localizable.strings */; }; CEA14870240780E100C6ECD2 /* ResourceInfoView.xib in Resources */ = {isa = PBXBuildFile; fileRef = CEA14872240780E100C6ECD2 /* ResourceInfoView.xib */; }; + CE9E95CD24CE786900F6DD78 /* UniversalLinks.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE9E95CC24CE786900F6DD78 /* UniversalLinks.swift */; }; + CE9E95CF24CE7A2C00F6DD78 /* UniversalLinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE9E95CE24CE7A2C00F6DD78 /* UniversalLinkTests.swift */; }; CEA6BA9D249872E8002D44CE /* Simple 13.0 Migration.bundle in Resources */ = {isa = PBXBuildFile; fileRef = CEA6BA9C249872E7002D44CE /* Simple 13.0 Migration.bundle */; }; CEA70CCF24CAC3AE001C12E6 /* Obsoletions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEA70CCE24CAC3AE001C12E6 /* Obsoletions.swift */; }; CEA9670924BEC4030035AACF /* ResourceUpdateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEA9670824BEC4030035AACF /* ResourceUpdateTests.swift */; }; @@ -515,6 +517,8 @@ CEA14871240780E100C6ECD2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/ResourceInfoView.xib; sourceTree = ""; }; CEA14874240780EF00C6ECD2 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/ResourceInfoView.strings; sourceTree = ""; }; CEA148772407869200C6ECD2 /* km */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = km; path = km.lproj/ResourceInfoView.strings; sourceTree = ""; }; + CE9E95CC24CE786900F6DD78 /* UniversalLinks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UniversalLinks.swift; sourceTree = ""; }; + CE9E95CE24CE7A2C00F6DD78 /* UniversalLinkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UniversalLinkTests.swift; sourceTree = ""; }; CEA6BA9C249872E7002D44CE /* Simple 13.0 Migration.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = "Simple 13.0 Migration.bundle"; sourceTree = ""; }; CEA70CCE24CAC3AE001C12E6 /* Obsoletions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Obsoletions.swift; sourceTree = ""; }; CEA9670824BEC4030035AACF /* ResourceUpdateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourceUpdateTests.swift; sourceTree = ""; }; @@ -778,6 +782,7 @@ CED8B63124A9C2400054E300 /* ResourceDownloadManagerTests.swift */, CEA9670824BEC4030035AACF /* ResourceUpdateTests.swift */, CE88143924A9B7F4002809C3 /* ResourceDownloadQueueTests.swift */, + CE9E95CE24CE7A2C00F6DD78 /* UniversalLinkTests.swift */, ); path = KeymanEngineTests; sourceTree = ""; @@ -1096,6 +1101,7 @@ CE79B24823C711FF007E72AE /* KeyboardScaleMap.swift */, CE7ADD6523DE89FC00BC9A00 /* Alerts.swift */, CEA70CCE24CAC3AE001C12E6 /* Obsoletions.swift */, + CE9E95CC24CE786900F6DD78 /* UniversalLinks.swift */, ); path = Classes; sourceTree = ""; @@ -1498,6 +1504,7 @@ CEDFEF8F23FE43B700BECF39 /* MigrationTests.swift in Sources */, CED8B63224A9C2400054E300 /* ResourceDownloadManagerTests.swift in Sources */, CE9CD88023FCC1CA002BF2F8 /* TestUtils.swift in Sources */, + CE9E95CF24CE7A2C00F6DD78 /* UniversalLinkTests.swift in Sources */, CE973D812484A56500F66045 /* PackageJSON.swift in Sources */, CE88143A24A9B7F4002809C3 /* ResourceDownloadQueueTests.swift in Sources */, CE8EDEB123F53D1A009E1FF6 /* FileManagementTests.swift in Sources */, @@ -1540,6 +1547,7 @@ C0B901AA1FA1AFC200764EB8 /* UserDefaults+Types.swift in Sources */, CE1F67A32304EB3800FF6972 /* ResourceDownloadManager.swift in Sources */, 9A079E372238680700581263 /* KMPLexicalModel.swift in Sources */, + CE9E95CD24CE786900F6DD78 /* UniversalLinks.swift in Sources */, CE89641F24A4686000D5EB8E /* Queries.swift in Sources */, CE5C8BE324B5B3BA00FAFB7F /* Queries+LexicalModel.swift in Sources */, 9A3E832522EAC14A00D22D2A /* KeyboardSwitcherViewController.swift in Sources */, diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/KeyboardSearchViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/KeyboardSearchViewController.swift index 7a15950f0e..e505172194 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/KeyboardSearchViewController.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/KeyboardSearchViewController.swift @@ -80,8 +80,6 @@ public class KeyboardSearchViewController: UIViewController, WKNavigationDelegat return baseURL } - private static let REGEX_FOR_DOWNLOAD_INTERCEPT = try! NSRegularExpression(pattern: "^http(?:s)?:\\/\\/[^\\/]+\\/keyboards\\/install\\/([^?\\/]+)(?:\\?(.+))?$") - public init(languageCode: String? = nil, withSession session: URLSession = URLSession.shared, keyboardSelectionBlock: @escaping SelectionCompletedHandler) { @@ -115,13 +113,13 @@ public class KeyboardSearchViewController: UIViewController, WKNavigationDelegat decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { if navigationAction.navigationType == .linkActivated { let link = navigationAction.request.url! - if let (keyboard_id, lang_id) = KeyboardSearchViewController.tryParseLink(link) { + if let parsedLink = UniversalLinks.tryParseKeyboardInstallLink(link) { decisionHandler(.cancel) // Notify our caller of the search results. self.hasFinalized = true // Prevent popViewController from triggering cancellation events. self.navigationController?.popViewController(animated: true) // Rewind UI - finalize(with: keyboard_id, for: lang_id) + finalize(with: parsedLink) return } } @@ -136,35 +134,12 @@ public class KeyboardSearchViewController: UIViewController, WKNavigationDelegat } } - internal static func tryParseLink(_ link: URL) -> (String, String?)? { - let linkString = link.absoluteString - - // If it matches the format for the Keyboard Universal Link URL Pattern... - // (see https://docs.google.com/document/d/1rhgMeJlCdXCi6ohPb_CuyZd0PZMoSzMqGpv1A8cMFHY/edit?ts=5f11cb13#heading=h.qw7pas2adckj) - if let match = REGEX_FOR_DOWNLOAD_INTERCEPT.firstMatch(in: linkString, - options: [], - range: NSRange(location: 0, length: linkString.utf16.count)) { - let keyboard_id_range = Range(match.range(at: 1), in: linkString)! - let keyboard_id = String(linkString[keyboard_id_range]) - - var lang_id: String? = nil - let urlComponents = URLComponents(string: linkString)! - if let lang_id_component = urlComponents.queryItems?.first(where: { $0.name == "bcp47" }) { - lang_id = lang_id_component.value - } - - return (keyboard_id, lang_id) - } else { - return nil - } - } - - internal func finalize(with keyboard_id: String, for lang_id: String?) { - let packageKey = KeymanPackage.Key(id: keyboard_id, type: .keyboard) + internal func finalize(with parsedLink: UniversalLinks.ParsedKeyboardInstallLink) { + let packageKey = KeymanPackage.Key(id: parsedLink.keyboard_id, type: .keyboard) // If we have a language ID AND do not yet have a model for it. - if let lang_id = lang_id { - let resourceKey = FullKeyboardID(keyboardID: keyboard_id, languageID: lang_id) + if let lang_id = parsedLink.lang_id { + let resourceKey = FullKeyboardID(keyboardID: parsedLink.keyboard_id, languageID: lang_id) let kbdURL = ResourceDownloadManager.shared.defaultDownloadURL(forPackage: packageKey, andResource: resourceKey, asUpdate: false) self.keyboardSelectionClosure(.tagged(packageKey, kbdURL, resourceKey)) } else { diff --git a/ios/engine/KMEI/KeymanEngine/Classes/UniversalLinks.swift b/ios/engine/KMEI/KeymanEngine/Classes/UniversalLinks.swift new file mode 100644 index 0000000000..939fee11bd --- /dev/null +++ b/ios/engine/KMEI/KeymanEngine/Classes/UniversalLinks.swift @@ -0,0 +1,41 @@ +// +// UniversalLinks.swift +// KeymanEngine +// +// Created by Joshua Horton on 7/27/20. +// Copyright © 2020 SIL International. All rights reserved. +// + +import Foundation + +public class UniversalLinks { + public struct ParsedKeyboardInstallLink { + let keyboard_id: String + let lang_id: String? + } + + private static let KEYBOARD_INSTALL_LINK_REGEX = try! NSRegularExpression(pattern: "^http(?:s)?:\\/\\/[^\\/]+\\/keyboards\\/install\\/([^?\\/]+)(?:\\?(.+))?$") + + public static func tryParseKeyboardInstallLink(_ link: URL) -> ParsedKeyboardInstallLink? { + let linkString = link.absoluteString + + // If it matches the format for the Keyboard Universal Link URL Pattern... + // (see https://docs.google.com/document/d/1rhgMeJlCdXCi6ohPb_CuyZd0PZMoSzMqGpv1A8cMFHY/edit?ts=5f11cb13#heading=h.qw7pas2adckj) + if let match = KEYBOARD_INSTALL_LINK_REGEX.firstMatch(in: linkString, + options: [], + range: NSRange(location: 0, length: linkString.utf16.count)) { + let keyboard_id_range = Range(match.range(at: 1), in: linkString)! + let keyboard_id = String(linkString[keyboard_id_range]) + + var lang_id: String? = nil + let urlComponents = URLComponents(string: linkString)! + if let lang_id_component = urlComponents.queryItems?.first(where: { $0.name == "bcp47" }) { + lang_id = lang_id_component.value + } + + return ParsedKeyboardInstallLink(keyboard_id: keyboard_id, lang_id: lang_id) + } else { + return nil + } + } +} diff --git a/ios/engine/KMEI/KeymanEngineTests/KeyboardSearchTests.swift b/ios/engine/KMEI/KeymanEngineTests/KeyboardSearchTests.swift index 143e3742de..e22008e0fc 100644 --- a/ios/engine/KMEI/KeymanEngineTests/KeyboardSearchTests.swift +++ b/ios/engine/KMEI/KeymanEngineTests/KeyboardSearchTests.swift @@ -32,40 +32,6 @@ class KeyboardSearchTests: XCTestCase { } } - func testTryLinkParse() { - var tuple = KeyboardSearchViewController.tryParseLink(URL.init(string: "\(KeymanHosts.KEYMAN_COM)/randomURL")!) - - XCTAssertNil(tuple) - - tuple = KeyboardSearchViewController.tryParseLink(URL.init(string: "\(KeymanHosts.KEYMAN_COM)/keyboards/install/khmer_angkor")!) - - if let tuple = tuple { - XCTAssertEqual(tuple.0, "khmer_angkor") - XCTAssertNil(tuple.1) - } - - tuple = KeyboardSearchViewController.tryParseLink(URL.init(string: "\(KeymanHosts.KEYMAN_COM)/keyboards/install/khmer_angkor?bcp47=km")!) - - if let tuple = tuple { - XCTAssertEqual(tuple.0, "khmer_angkor") - XCTAssertEqual(tuple.1, "km") - } - - tuple = KeyboardSearchViewController.tryParseLink(URL.init(string: "\(KeymanHosts.KEYMAN_COM)/keyboards/install/sil_euro_latin")!) - - if let tuple = tuple { - XCTAssertEqual(tuple.0, "sil_euro_latin") - XCTAssertNil(tuple.1) - } - - tuple = KeyboardSearchViewController.tryParseLink(URL.init(string: "\(KeymanHosts.KEYMAN_COM)/keyboards/install/foo?bcp47=bar")!) - - if let tuple = tuple { - XCTAssertEqual(tuple.0, "foo") - XCTAssertEqual(tuple.1, "bar") - } - } - func testFinalizeNoLanguage() { let kbdExpectation = XCTestExpectation() @@ -84,7 +50,7 @@ class KeyboardSearchTests: XCTestCase { let searchNoLang = KeyboardSearchViewController(languageCode: nil, withSession: mockedURLSession!, keyboardSelectionBlock: kbdBlock) - searchNoLang.finalize(with: "khmer_angkor", for: nil) + searchNoLang.finalize(with: UniversalLinks.ParsedKeyboardInstallLink(keyboard_id: "khmer_angkor", lang_id: nil), for: nil) wait(for: [kbdExpectation], timeout: 5) } @@ -107,7 +73,7 @@ class KeyboardSearchTests: XCTestCase { let search = KeyboardSearchViewController(languageCode: nil, withSession: mockedURLSession!, keyboardSelectionBlock: kbdBlock) - search.finalize(with: "sil_euro_latin", for: "en") + search.finalize(with: UniversalLinks.ParsedKeyboardInstallLink(keyboard_id: "sil_euro_latin", for: "en") wait(for: [kbdExpectation], timeout: 5) } diff --git a/ios/engine/KMEI/KeymanEngineTests/UniversalLinkTests.swift b/ios/engine/KMEI/KeymanEngineTests/UniversalLinkTests.swift new file mode 100644 index 0000000000..00bb2bf4a1 --- /dev/null +++ b/ios/engine/KMEI/KeymanEngineTests/UniversalLinkTests.swift @@ -0,0 +1,46 @@ +// +// UniversalLinkTests.swift +// KeymanEngineTests +// +// Created by Joshua Horton on 7/27/20. +// Copyright © 2020 SIL International. All rights reserved. +// + +import XCTest +@testable import KeymanEngine + +class UniversalLinkTests: XCTestCase { + func testTryLinkParse() { + var parsedLink = UniversalLinks.tryParseKeyboardInstallLink(URL.init(string: "\(KeymanHosts.KEYMAN_COM)/randomURL")!) + + XCTAssertNil(parsedLink) + + parsedLink = UniversalLinks.tryParseKeyboardInstallLink(URL.init(string: "\(KeymanHosts.KEYMAN_COM)/keyboards/install/khmer_angkor")!) + + if let parsedLink = parsedLink { + XCTAssertEqual(parsedLink.keyboard_id, "khmer_angkor") + XCTAssertNil(parsedLink.lang_id) + } + + parsedLink = UniversalLinks.tryParseKeyboardInstallLink(URL.init(string: "\(KeymanHosts.KEYMAN_COM)/keyboards/install/khmer_angkor?bcp47=km")!) + + if let parsedLink = parsedLink { + XCTAssertEqual(parsedLink.keyboard_id, "khmer_angkor") + XCTAssertEqual(parsedLink.lang_id, "km") + } + + parsedLink = UniversalLinks.tryParseKeyboardInstallLink(URL.init(string: "\(KeymanHosts.KEYMAN_COM)/keyboards/install/sil_euro_latin")!) + + if let parsedLink = parsedLink { + XCTAssertEqual(parsedLink.keyboard_id, "sil_euro_latin") + XCTAssertNil(parsedLink.lang_id) + } + + parsedLink = UniversalLinks.tryParseKeyboardInstallLink(URL.init(string: "\(KeymanHosts.KEYMAN_COM)/keyboards/install/foo?bcp47=bar")!) + + if let parsedLink = parsedLink { + XCTAssertEqual(parsedLink.keyboard_id, "foo") + XCTAssertEqual(parsedLink.lang_id, "bar") + } + } +} From bb3a13900d5805204fd5ec7f03805b05ed486fb2 Mon Sep 17 00:00:00 2001 From: jahorton Date: Mon, 27 Jul 2020 10:37:20 +0700 Subject: [PATCH 2/7] feat(ios/app): adds entitlement, AppDelegate hook --- ios/keyman/Keyman/Keyman/AppDelegate.swift | 21 ++++++++++++++++++++ ios/keyman/Keyman/Keyman/Keyman.entitlements | 5 +++++ 2 files changed, 26 insertions(+) diff --git a/ios/keyman/Keyman/Keyman/AppDelegate.swift b/ios/keyman/Keyman/Keyman/AppDelegate.swift index 4c68ae374f..e959c2685e 100644 --- a/ios/keyman/Keyman/Keyman/AppDelegate.swift +++ b/ios/keyman/Keyman/Keyman/AppDelegate.swift @@ -88,6 +88,27 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return true } + // Handles universal links. + func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { + if userActivity.activityType == NSUserActivityTypeBrowsingWeb { + guard let incomingURL = userActivity.webpageURL else { + return false + } + + if let parsedLink = UniversalLinks.tryParseKeyboardInstallLink(incomingURL) { + // Aha! We know this link type! + + // TODO: Things. Note - here, there are no pre-existing UI expectations, so while + // what we need to do is _functionally_ similiar to what keyboard-search does + // after clicking a link, the integration requirements are pretty distinct. + + return true + } + } + + return false + } + func applicationDidEnterBackground(_ application: UIApplication) { _overlayWindow = nil FontManager.shared.unregisterCustomFonts() diff --git a/ios/keyman/Keyman/Keyman/Keyman.entitlements b/ios/keyman/Keyman/Keyman/Keyman.entitlements index ccdcdabc00..326f39220d 100644 --- a/ios/keyman/Keyman/Keyman/Keyman.entitlements +++ b/ios/keyman/Keyman/Keyman/Keyman.entitlements @@ -2,6 +2,11 @@ + com.apple.developer.associated-domains + + applinks:keyman.com + applinks:keyman-staging.com + com.apple.security.application-groups group.KM4I From b7b9de23c0fd16f3e9527e1221380beff0d5b4b2 Mon Sep 17 00:00:00 2001 From: jahorton Date: Thu, 6 Aug 2020 13:36:38 +0700 Subject: [PATCH 3/7] chore(ios/engine): post-rebase patchup --- ios/engine/KMEI/KeymanEngineTests/KeyboardSearchTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ios/engine/KMEI/KeymanEngineTests/KeyboardSearchTests.swift b/ios/engine/KMEI/KeymanEngineTests/KeyboardSearchTests.swift index e22008e0fc..88e262f1e5 100644 --- a/ios/engine/KMEI/KeymanEngineTests/KeyboardSearchTests.swift +++ b/ios/engine/KMEI/KeymanEngineTests/KeyboardSearchTests.swift @@ -50,7 +50,7 @@ class KeyboardSearchTests: XCTestCase { let searchNoLang = KeyboardSearchViewController(languageCode: nil, withSession: mockedURLSession!, keyboardSelectionBlock: kbdBlock) - searchNoLang.finalize(with: UniversalLinks.ParsedKeyboardInstallLink(keyboard_id: "khmer_angkor", lang_id: nil), for: nil) + searchNoLang.finalize(with: UniversalLinks.ParsedKeyboardInstallLink(keyboard_id: "khmer_angkor", lang_id: nil)) wait(for: [kbdExpectation], timeout: 5) } @@ -73,7 +73,7 @@ class KeyboardSearchTests: XCTestCase { let search = KeyboardSearchViewController(languageCode: nil, withSession: mockedURLSession!, keyboardSelectionBlock: kbdBlock) - search.finalize(with: UniversalLinks.ParsedKeyboardInstallLink(keyboard_id: "sil_euro_latin", for: "en") + search.finalize(with: UniversalLinks.ParsedKeyboardInstallLink(keyboard_id: "sil_euro_latin", lang_id: "en")) wait(for: [kbdExpectation], timeout: 5) } From 818ae537528002d43ab8d3b0c652d95b221c1a98 Mon Sep 17 00:00:00 2001 From: jahorton Date: Thu, 6 Aug 2020 13:58:35 +0700 Subject: [PATCH 4/7] feat(ios/app): operational universal links --- .../Classes/Model/FullKeyboardID.swift | 5 ++++ .../KeymanEngine/Classes/UniversalLinks.swift | 8 +++-- ios/keyman/Keyman/Keyman/AppDelegate.swift | 29 +++++++++++++++++-- 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Model/FullKeyboardID.swift b/ios/engine/KMEI/KeymanEngine/Classes/Model/FullKeyboardID.swift index e97f53e9a0..67f98e7c54 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Model/FullKeyboardID.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Model/FullKeyboardID.swift @@ -22,6 +22,11 @@ public struct FullKeyboardID: Codable, LanguageResourceFullID, Equatable { public var type: LanguageResourceType { return .keyboard } + + public init(keyboardID: String, languageID: String) { + self.keyboardID = keyboardID + self.languageID = languageID + } } // MARK: - CustomStringConvertible diff --git a/ios/engine/KMEI/KeymanEngine/Classes/UniversalLinks.swift b/ios/engine/KMEI/KeymanEngine/Classes/UniversalLinks.swift index 939fee11bd..0e82bff10b 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/UniversalLinks.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/UniversalLinks.swift @@ -10,8 +10,12 @@ import Foundation public class UniversalLinks { public struct ParsedKeyboardInstallLink { - let keyboard_id: String - let lang_id: String? + public let keyboard_id: String + public let lang_id: String? + + public var packageKey: KeymanPackage.Key { + return KeymanPackage.Key(id: keyboard_id, type: .keyboard) + } } private static let KEYBOARD_INSTALL_LINK_REGEX = try! NSRegularExpression(pattern: "^http(?:s)?:\\/\\/[^\\/]+\\/keyboards\\/install\\/([^?\\/]+)(?:\\?(.+))?$") diff --git a/ios/keyman/Keyman/Keyman/AppDelegate.swift b/ios/keyman/Keyman/Keyman/AppDelegate.swift index e959c2685e..5e50d913f9 100644 --- a/ios/keyman/Keyman/Keyman/AppDelegate.swift +++ b/ios/keyman/Keyman/Keyman/AppDelegate.swift @@ -97,11 +97,34 @@ class AppDelegate: UIResponder, UIApplicationDelegate { if let parsedLink = UniversalLinks.tryParseKeyboardInstallLink(incomingURL) { // Aha! We know this link type! + let packageKey = parsedLink.packageKey - // TODO: Things. Note - here, there are no pre-existing UI expectations, so while - // what we need to do is _functionally_ similiar to what keyboard-search does - // after clicking a link, the integration requirements are pretty distinct. + let downloadLink: URL + if let langID = parsedLink.lang_id { + let fullID = FullKeyboardID(keyboardID: parsedLink.keyboard_id, languageID: langID) + downloadLink = ResourceDownloadManager.shared.defaultDownloadURL(forPackage: parsedLink.packageKey, + andResource: fullID, + asUpdate: false) + } else { + downloadLink = ResourceDownloadManager.shared.defaultDownloadURL(forPackage: parsedLink.packageKey, + asUpdate: false) + } + ResourceDownloadManager.shared.downloadPackage(withKey: parsedLink.packageKey, from: downloadLink) { (package: KeyboardKeymanPackage?, error: Error?) in + guard error == nil, let package = package else { + // Maybe add an alert about the package error? + return + } + if let vc = self.window?.rootViewController { + // Force the app to the top-level view. (Prompts won't display if we're in a submenu!) + vc.dismiss(animated: true, completion: nil) + // We choose to prompt the user for comfirmation, rather + // than automatically installing the package. + ResourceFileManager.shared.promptPackageInstall(of: package, in: vc, isCustom: true) + } else { + log.error("Cannot find app's root UIViewController") + } + } return true } } From 20a8cdead155c30792bd6fd8cd1f0a831ee268c4 Mon Sep 17 00:00:00 2001 From: jahorton Date: Thu, 6 Aug 2020 14:10:09 +0700 Subject: [PATCH 5/7] fix(ios/app): matching fix for universal link case --- ios/keyman/Keyman/Keyman/AppDelegate.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ios/keyman/Keyman/Keyman/AppDelegate.swift b/ios/keyman/Keyman/Keyman/AppDelegate.swift index 9fe125ddd2..eded73af76 100644 --- a/ios/keyman/Keyman/Keyman/AppDelegate.swift +++ b/ios/keyman/Keyman/Keyman/AppDelegate.swift @@ -124,7 +124,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // We choose to prompt the user for comfirmation, rather // than automatically installing the package. - ResourceFileManager.shared.promptPackageInstall(of: package, in: vc, isCustom: true) + let nvc = UINavigationController.init() + ResourceFileManager.shared.promptPackageInstall(of: package, in: nvc, isCustom: true) + vc.present(nvc, animated: true, completion: nil) } else { log.error("Cannot find app's root UIViewController") } From 4f4ca3e626f9bc231c3da3bde8a57d4f8d75305a Mon Sep 17 00:00:00 2001 From: jahorton Date: Fri, 7 Aug 2020 08:10:53 +0700 Subject: [PATCH 6/7] feat(ios/engine): extra link-parse unit tests --- .../UniversalLinkTests.swift | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/ios/engine/KMEI/KeymanEngineTests/UniversalLinkTests.swift b/ios/engine/KMEI/KeymanEngineTests/UniversalLinkTests.swift index 00bb2bf4a1..7512121ead 100644 --- a/ios/engine/KMEI/KeymanEngineTests/UniversalLinkTests.swift +++ b/ios/engine/KMEI/KeymanEngineTests/UniversalLinkTests.swift @@ -20,6 +20,8 @@ class UniversalLinkTests: XCTestCase { if let parsedLink = parsedLink { XCTAssertEqual(parsedLink.keyboard_id, "khmer_angkor") XCTAssertNil(parsedLink.lang_id) + } else { + XCTFail() } parsedLink = UniversalLinks.tryParseKeyboardInstallLink(URL.init(string: "\(KeymanHosts.KEYMAN_COM)/keyboards/install/khmer_angkor?bcp47=km")!) @@ -27,6 +29,8 @@ class UniversalLinkTests: XCTestCase { if let parsedLink = parsedLink { XCTAssertEqual(parsedLink.keyboard_id, "khmer_angkor") XCTAssertEqual(parsedLink.lang_id, "km") + } else { + XCTFail() } parsedLink = UniversalLinks.tryParseKeyboardInstallLink(URL.init(string: "\(KeymanHosts.KEYMAN_COM)/keyboards/install/sil_euro_latin")!) @@ -34,6 +38,8 @@ class UniversalLinkTests: XCTestCase { if let parsedLink = parsedLink { XCTAssertEqual(parsedLink.keyboard_id, "sil_euro_latin") XCTAssertNil(parsedLink.lang_id) + } else { + XCTFail() } parsedLink = UniversalLinks.tryParseKeyboardInstallLink(URL.init(string: "\(KeymanHosts.KEYMAN_COM)/keyboards/install/foo?bcp47=bar")!) @@ -41,6 +47,28 @@ class UniversalLinkTests: XCTestCase { if let parsedLink = parsedLink { XCTAssertEqual(parsedLink.keyboard_id, "foo") XCTAssertEqual(parsedLink.lang_id, "bar") + } else { + XCTFail() + } + } + + func testTryLinkParseWithExtraneousComponents() { + var parsedLink = UniversalLinks.tryParseKeyboardInstallLink(URL.init(string: "\(KeymanHosts.KEYMAN_COM)/keyboards/install/foo?bcp47=bar&baz=nope")!) + + if let parsedLink = parsedLink { + XCTAssertEqual(parsedLink.keyboard_id, "foo") + XCTAssertEqual(parsedLink.lang_id, "bar") + } else { + XCTFail() + } + + parsedLink = UniversalLinks.tryParseKeyboardInstallLink(URL.init(string: "\(KeymanHosts.KEYMAN_COM)/keyboards/install/foo?_t=123&bcp47=bar")!) + + if let parsedLink = parsedLink { + XCTAssertEqual(parsedLink.keyboard_id, "foo") + XCTAssertEqual(parsedLink.lang_id, "bar") + } else { + XCTFail() } } } From 54b8ac8302be70f10e3e17a4b0167ef91bc83774 Mon Sep 17 00:00:00 2001 From: jahorton Date: Fri, 7 Aug 2020 09:40:51 +0700 Subject: [PATCH 7/7] fix(ios/app): lint warning fixes --- ios/keyman/Keyman/Keyman/AppDelegate.swift | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/ios/keyman/Keyman/Keyman/AppDelegate.swift b/ios/keyman/Keyman/Keyman/AppDelegate.swift index eded73af76..e0b9e67d59 100644 --- a/ios/keyman/Keyman/Keyman/AppDelegate.swift +++ b/ios/keyman/Keyman/Keyman/AppDelegate.swift @@ -93,27 +93,30 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } // Handles universal links. - func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { + func application(_ application: UIApplication, + continue userActivity: NSUserActivity, + restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { if userActivity.activityType == NSUserActivityTypeBrowsingWeb { guard let incomingURL = userActivity.webpageURL else { return false } if let parsedLink = UniversalLinks.tryParseKeyboardInstallLink(incomingURL) { - // Aha! We know this link type! - let packageKey = parsedLink.packageKey + // We use this mostly to shorten line lengths, b/c lint warnings. + let downloadManager = ResourceDownloadManager.shared + // Aha! We know this link type! let downloadLink: URL if let langID = parsedLink.lang_id { let fullID = FullKeyboardID(keyboardID: parsedLink.keyboard_id, languageID: langID) - downloadLink = ResourceDownloadManager.shared.defaultDownloadURL(forPackage: parsedLink.packageKey, - andResource: fullID, - asUpdate: false) + downloadLink = downloadManager.defaultDownloadURL(forPackage: parsedLink.packageKey, + andResource: fullID, + asUpdate: false) } else { - downloadLink = ResourceDownloadManager.shared.defaultDownloadURL(forPackage: parsedLink.packageKey, - asUpdate: false) + downloadLink = downloadManager.defaultDownloadURL(forPackage: parsedLink.packageKey, asUpdate: false) } - ResourceDownloadManager.shared.downloadPackage(withKey: parsedLink.packageKey, from: downloadLink) { (package: KeyboardKeymanPackage?, error: Error?) in + downloadManager.downloadPackage(withKey: parsedLink.packageKey, + from: downloadLink) { (package: KeyboardKeymanPackage?, error: Error?) in guard error == nil, let package = package else { // Maybe add an alert about the package error? return