From c01fb9570ba1a68b6a14affd675d8b9cc8a413e1 Mon Sep 17 00:00:00 2001 From: jahorton Date: Fri, 8 Nov 2019 15:57:13 +0700 Subject: [PATCH 01/26] feat(web): start web dark mode css dev --- web/source/resources/osk/kmwosk.css | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/web/source/resources/osk/kmwosk.css b/web/source/resources/osk/kmwosk.css index dfbb0a2793..7f403bcdd7 100644 --- a/web/source/resources/osk/kmwosk.css +++ b/web/source/resources/osk/kmwosk.css @@ -64,6 +64,16 @@ .phone.ios .kmw-key.kmw-key-special-on {color:#000;background-color:#fdfdfe;} .phone.ios .kmw-key.kmw-key-touched {background-color:#88f;} +@media (prefers-color-scheme: dark) { + .phone.ios .kmw-key-layer-group {background-color: #0f1319} + .phone.ios .kmw-key.kmw-key-default {color:#fff;background-color:#3d3d3e} + .phone.ios .kmw-key.kmw-key-shift, + .phone.ios .kmw-key.kmw-key-special {color:#fff;background-color:#595c62;} + .phone.ios .kmw-key.kmw-key-shift-on, + .phone.ios .kmw-key.kmw-key-special-on {color:#fff;background-color:#3d3d3e;} + .phone.ios .kmw-key.kmw-key-touched {background-color:#447;} +} + .ios .kmw-banner-bar { background-color: #cfd3d9; position: absolute; @@ -268,19 +278,19 @@ body div.kmw-key-shift-on span.kmw-key-text {font-family:SpecialOSK !important;f #kmw-popup-callout { position:fixed; display:block; background-color: #fdfdfe; border-radius: 0 0 6px 6px; z-index:10001; pointer-events:none; - } +} /* Key preview styles */ div.ios div.kmw-keytip { position:fixed; left:0; top:0; width:3em; height:3em; background-color:rgb(0,0,0,0);overflow:visible; - } +} div.android div.kmw-keytip { position:fixed; left:0; top:0; width:3em; height:3em; color:#fff; background-color: rgb(0,0,0,0); overflow:visible; - } +} div.android #keytip {background-color:#f00;} @@ -358,6 +368,10 @@ div.android #keytip {background-color:#f00;} .kmw-spacebar-caption{font:0.6em Arial !important;color:rgba(0,0,0,0.15);} +@media (prefers-color-scheme: dark) { + .kmw-spacebar-caption{color:rgba(255, 255, 255, 0.15);} +} + /* Static styles */ .desktop-static.kmw-osk-inner-frame{margin:0;background-color:#666;border:2px solid #ad4a28;box-sizing:border-box;-moz-box-sizing:border-box;} From 966ea1277ed566bd07cd1883596130cdf8bd5476 Mon Sep 17 00:00:00 2001 From: jahorton Date: Fri, 8 Nov 2019 16:04:58 +0700 Subject: [PATCH 02/26] docs(web): an important thought re dark mode implementation in web --- web/source/resources/osk/kmwosk.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/source/resources/osk/kmwosk.css b/web/source/resources/osk/kmwosk.css index 7f403bcdd7..cd59ff5232 100644 --- a/web/source/resources/osk/kmwosk.css +++ b/web/source/resources/osk/kmwosk.css @@ -64,6 +64,7 @@ .phone.ios .kmw-key.kmw-key-special-on {color:#000;background-color:#fdfdfe;} .phone.ios .kmw-key.kmw-key-touched {background-color:#88f;} +/* Probably best to make this its own CSS that can be optionally included? */ @media (prefers-color-scheme: dark) { .phone.ios .kmw-key-layer-group {background-color: #0f1319} .phone.ios .kmw-key.kmw-key-default {color:#fff;background-color:#3d3d3e} @@ -368,6 +369,7 @@ div.android #keytip {background-color:#f00;} .kmw-spacebar-caption{font:0.6em Arial !important;color:rgba(0,0,0,0.15);} +/* Probably best to make this its own CSS that can be optionally included? */ @media (prefers-color-scheme: dark) { .kmw-spacebar-caption{color:rgba(255, 255, 255, 0.15);} } From 54c3933379eaeb94cc622b241196e473928bf3d0 Mon Sep 17 00:00:00 2001 From: jahorton Date: Fri, 6 Dec 2019 12:49:35 +0700 Subject: [PATCH 03/26] 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 05f50f03f4fcab43a3f2d030556e54f2b3b08e49 Mon Sep 17 00:00:00 2001 From: Darcy Wong Date: Thu, 12 Dec 2019 10:45:09 +0700 Subject: [PATCH 04/26] fix(android): Investigate propagating custom help links --- .../kmea/KMKeyboardDownloaderActivity.java | 2 +- .../kmea/cloud/CloudDataJsonUtil.java | 11 ++++++++--- .../CloudKeyboardMetaDataDownloadCallback.java | 3 ++- .../java/com/tavultesoft/kmea/data/Keyboard.java | 15 ++++++++++++++- .../tavultesoft/kmea/data/LanguageResource.java | 1 + .../com/tavultesoft/kmea/data/LexicalModel.java | 16 +++++++++++++--- .../kmea/cloud/CloudDataJsonUtilTest.java | 3 ++- 7 files changed, 41 insertions(+), 10 deletions(-) diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMKeyboardDownloaderActivity.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMKeyboardDownloaderActivity.java index c2d7a1e9f9..c122381785 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMKeyboardDownloaderActivity.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMKeyboardDownloaderActivity.java @@ -33,7 +33,7 @@ public class KMKeyboardDownloaderActivity extends AppCompatActivity { public static final String ARG_MODEL_ID = "KMKeyboardActivity.modelID"; public static final String ARG_MODEL_NAME = "KMKeyboardActivity.modelName"; public static final String ARG_MODEL_URL = "KMKeyboardActivity.modelURL"; - public static final String ARG_MODEL_CUSTOM_HELP_LINK = "KMKeyboardActivity.customHelpLink"; + public static final String ARG_CUSTOM_HELP_LINK = "KMKeyboardActivity.customHelpLink"; // custom keyboard public static final String ARG_KEYBOARD = "KMKeyboardActivity.keyboard"; diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/cloud/CloudDataJsonUtil.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/cloud/CloudDataJsonUtil.java index a6070b6fc2..0a83d73733 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/cloud/CloudDataJsonUtil.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/cloud/CloudDataJsonUtil.java @@ -37,7 +37,7 @@ public class CloudDataJsonUtil { public static HashMap createKeyboardInfoMap(String aPackageId,String aLanguageId, String aLanguageName, String aKeyboardId, String aKeyboardName, String aKeyboardVersion, String anIsCustomKeyboard, - String aFont, String aOskFont) + String aFont, String aOskFont, String aCustomHelpLink) { HashMap keyboardInfo = new HashMap(); keyboardInfo.put(KMManager.KMKey_PackageID, aPackageId); @@ -48,8 +48,12 @@ public class CloudDataJsonUtil { keyboardInfo.put(KMManager.KMKey_KeyboardVersion, aKeyboardVersion); keyboardInfo.put(KMManager.KMKey_CustomKeyboard, anIsCustomKeyboard); keyboardInfo.put(KMManager.KMKey_Font, aFont); - if (aOskFont != null) + if (aOskFont != null) { keyboardInfo.put(KMManager.KMKey_OskFont, aOskFont); + } + if (aCustomHelpLink != null) { + keyboardInfo.put(KMManager.KMKey_CustomHelpLink, aCustomHelpLink); + } return keyboardInfo; } @@ -81,9 +85,10 @@ public class CloudDataJsonUtil { String kbName = keyboardJSON.getString(KMManager.KMKey_Name); String kbVersion = keyboardJSON.optString(KMManager.KMKey_KeyboardVersion, "1.0"); String kbFont = keyboardJSON.optString(KMManager.KMKey_Font, ""); + String customHelpLink = keyboardJSON.optString(KMManager.KMKey_CustomHelpLink, null); //String kbKey = String.format("%s_%s", langID, kbID); - HashMap hashMap = createKeyboardInfoMap(pkgID,langID,langName,kbID,kbName,kbVersion,isCustom,kbFont,null); + HashMap hashMap = createKeyboardInfoMap(pkgID,langID,langName,kbID,kbName,kbVersion,isCustom,kbFont,null, customHelpLink); // if (keyboardModifiedDates.get(kbID) == null) { diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/cloud/impl/CloudKeyboardMetaDataDownloadCallback.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/cloud/impl/CloudKeyboardMetaDataDownloadCallback.java index 07c7fb5f9c..4938057056 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/cloud/impl/CloudKeyboardMetaDataDownloadCallback.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/cloud/impl/CloudKeyboardMetaDataDownloadCallback.java @@ -271,12 +271,13 @@ public class CloudKeyboardMetaDataDownloadCallback implements ICloudDownloadCall String _font = _keyboard.optString(KMManager.KMKey_Font); String _oskFont = _keyboard.optString(KMManager.KMKey_OskFont); + String _customHelpLink = _keyboard.optString(KMManager.KMKey_CustomHelpLink, null); theKbData.additionalDownloadid = CloudKeyboardDataDownloadCallback.createDownloadId(_key_id); theKbData.keyboardInfo = CloudDataJsonUtil .createKeyboardInfoMap( - _pkgID, _lang_id, _langName, _key_id, _kbName, _kbVersion, _kbIsCustom, _font, _oskFont); + _pkgID, _lang_id, _langName, _key_id, _kbName, _kbVersion, _kbIsCustom, _font, _oskFont, _customHelpLink); theKbData.additionalDownloads = urls; } catch(JSONException _e) diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/Keyboard.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/Keyboard.java index fa088416a8..bed03bbbb4 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/Keyboard.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/Keyboard.java @@ -5,6 +5,7 @@ import android.os.Bundle; import com.tavultesoft.kmea.KMKeyboardDownloaderActivity; import com.tavultesoft.kmea.KMManager; import com.tavultesoft.kmea.KeyboardPickerActivity; +import com.tavultesoft.kmea.util.MapCompat; import java.io.Serializable; import java.util.Map; @@ -50,6 +51,13 @@ public class Keyboard implements Serializable, LanguageResource { return this.map.get(KMManager.KMKey_KeyboardName); } + public String getCustomHelpLink() { + if (this.map.containsKey(KMManager.KMKey_CustomHelpLink)) { + return this.map.get(KMManager.KMKey_CustomHelpLink); + } + return null; + } + public String getVersion() { return this.map.get(KMManager.KMKey_KeyboardVersion); } @@ -60,6 +68,7 @@ public class Keyboard implements Serializable, LanguageResource { public Bundle buildDownloadBundle() { Bundle bundle = new Bundle(); + bundle.putString(KMKeyboardDownloaderActivity.ARG_PKG_ID, getPackage()); bundle.putString(KMKeyboardDownloaderActivity.ARG_KB_ID, getResourceId()); bundle.putString(KMKeyboardDownloaderActivity.ARG_LANG_ID, getLanguageCode()); @@ -70,9 +79,13 @@ public class Keyboard implements Serializable, LanguageResource { if(isCustom == null) { isCustom = "N"; } - bundle.putBoolean(KMKeyboardDownloaderActivity.ARG_IS_CUSTOM, isCustom.equals("Y")); + String customHelpLink = map.get(KMManager.KMKey_CustomHelpLink); + if (customHelpLink != null) { + bundle.putString(KMKeyboardDownloaderActivity.ARG_CUSTOM_HELP_LINK, getCustomHelpLink()); + } + return bundle; } diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/LanguageResource.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/LanguageResource.java index 635c76580b..fb687637a9 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/LanguageResource.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/LanguageResource.java @@ -9,6 +9,7 @@ public interface LanguageResource { String getLanguageName(); String getVersion(); String getPackage(); + String getCustomHelpLink(); Bundle buildDownloadBundle(); } diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/LexicalModel.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/LexicalModel.java index 2dddca0ddb..876013eb56 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/LexicalModel.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/LexicalModel.java @@ -4,6 +4,7 @@ import android.os.Bundle; import com.tavultesoft.kmea.KMKeyboardDownloaderActivity; import com.tavultesoft.kmea.KMManager; +import com.tavultesoft.kmea.util.MapCompat; import java.io.Serializable; import java.util.Map; @@ -52,6 +53,13 @@ public class LexicalModel implements Serializable, LanguageResource { return this.map.get(KMManager.KMKey_PackageID); } + public String getCustomHelpLink() { + if (this.map.containsKey(KMManager.KMKey_CustomHelpLink)) { + return this.map.get(KMManager.KMKey_CustomHelpLink); + } + return null; + } + public Bundle buildDownloadBundle() { Bundle bundle = new Bundle(); @@ -64,8 +72,6 @@ public class LexicalModel implements Serializable, LanguageResource { return null; } - String customHelpLink = map.get(KMManager.KMKey_CustomHelpLink); - bundle.putString(KMKeyboardDownloaderActivity.ARG_PKG_ID, getPackage()); bundle.putString(KMKeyboardDownloaderActivity.ARG_MODEL_ID, getResourceId()); bundle.putString(KMKeyboardDownloaderActivity.ARG_LANG_ID, getLanguageCode()); @@ -73,7 +79,11 @@ public class LexicalModel implements Serializable, LanguageResource { bundle.putString(KMKeyboardDownloaderActivity.ARG_LANG_NAME, getLanguageName()); bundle.putBoolean(KMKeyboardDownloaderActivity.ARG_IS_CUSTOM, false); bundle.putString(KMKeyboardDownloaderActivity.ARG_MODEL_URL, modelURL); - bundle.putString(KMKeyboardDownloaderActivity.ARG_MODEL_CUSTOM_HELP_LINK, customHelpLink); + + String customHelpLink = map.get(KMManager.KMKey_CustomHelpLink); + if (customHelpLink != null) { + bundle.putString(KMKeyboardDownloaderActivity.ARG_CUSTOM_HELP_LINK, customHelpLink); + } return bundle; } diff --git a/android/KMEA/app/src/test/java/com/tavultesoft/kmea/cloud/CloudDataJsonUtilTest.java b/android/KMEA/app/src/test/java/com/tavultesoft/kmea/cloud/CloudDataJsonUtilTest.java index dc41a7fdaa..906f965bad 100644 --- a/android/KMEA/app/src/test/java/com/tavultesoft/kmea/cloud/CloudDataJsonUtilTest.java +++ b/android/KMEA/app/src/test/java/com/tavultesoft/kmea/cloud/CloudDataJsonUtilTest.java @@ -30,6 +30,7 @@ public class CloudDataJsonUtilTest { private final String customKeyboard = "N"; private final String aFont = "AndikaAfr"; private final String oskFont = aFont; + private final String customHelpLink = ""; @Before public void initializeTestPackage() { @@ -40,7 +41,7 @@ public class CloudDataJsonUtilTest { public void shouldLowercaseLanguageID() { // Test createKeyboardInfoMap() used by processKeyboardJSON() HashMap kbInfo = CloudDataJsonUtil.createKeyboardInfoMap( - pkgID, langID, langName, keyboardID, keyboardName, keyboardVersion, customKeyboard, aFont, oskFont); + pkgID, langID, langName, keyboardID, keyboardName, keyboardVersion, customKeyboard, aFont, oskFont, customHelpLink); Assert.assertEquals(langID.toLowerCase(), kbInfo.get(KMManager.KMKey_LanguageID)); // Test processLexicalModelJSON() From 4eef1a1908d8c8470bb6f369ea63dbf705de1071 Mon Sep 17 00:00:00 2001 From: Darcy Wong Date: Thu, 12 Dec 2019 16:37:41 +0700 Subject: [PATCH 05/26] chore: update history.md --- android/history.md | 1 + 1 file changed, 1 insertion(+) diff --git a/android/history.md b/android/history.md index 34e63e543f..e1d3e55b84 100644 --- a/android/history.md +++ b/android/history.md @@ -20,6 +20,7 @@ * Add linting to Debug builds and resolve lint errors (#2305) * Fix memory issues during build process (#2361) * Fix crashes when parsing JSON data from Cloud (#2393) + * Propagate custom help links (#2448) ## 2019-12-12 12.0.4214 stable * Bug fix: From 518664930f540acd335e42a2b5e7c8ab4519302d Mon Sep 17 00:00:00 2001 From: jahorton Date: Tue, 17 Dec 2019 10:36:10 +0700 Subject: [PATCH 06/26] 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 07/26] 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 08/26] 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 09/26] 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 10/26] 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 11/26] 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 12/26] 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 13/26] 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 14/26] 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 6cc8a566814d261153377bc405043422e2bb297a Mon Sep 17 00:00:00 2001 From: Darcy Wong Date: Thu, 19 Dec 2019 15:55:08 +0700 Subject: [PATCH 15/26] fix(android): Update file provider permissions --- .../kmea/KeyboardInfoActivity.java | 43 ++++++++++++++++++- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KeyboardInfoActivity.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KeyboardInfoActivity.java index 090581b02e..a6fd61f124 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KeyboardInfoActivity.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KeyboardInfoActivity.java @@ -5,18 +5,21 @@ package com.tavultesoft.kmea; import java.io.File; +import java.io.FileFilter; import java.util.ArrayList; import java.util.HashMap; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; +import androidx.core.content.FileProvider; +import android.content.ClipData; +import android.content.ClipDescription; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.Typeface; import android.net.Uri; import android.os.Bundle; -import androidx.core.content.FileProvider; import android.util.Log; import android.view.View; import android.view.ViewGroup; @@ -52,7 +55,6 @@ public final class KeyboardInfoActivity extends AppCompatActivity { super.onCreate(savedInstanceState); supportRequestWindowFeature(Window.FEATURE_NO_TITLE); final Context context = this; - final String authority = FileProviderUtils.getAuthority(context); setContentView(R.layout.activity_list_layout); toolbar = (Toolbar) findViewById(R.id.list_toolbar); @@ -129,14 +131,51 @@ public final class KeyboardInfoActivity extends AppCompatActivity { Intent i = new Intent(Intent.ACTION_VIEW); if (customHelpLink != null) { + // Display local welcome.htm help file, including associated assets if (FileUtils.isWelcomeFile(customHelpLink) && ! KMManager.isTestMode()) { File customHelp = new File(new File(customHelpLink).getAbsolutePath()); i.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // Starting with Android N, you can't pass file:// to intents, so we use FileProvider try { + final String authority = FileProviderUtils.getAuthority(context); Uri contentUri = FileProvider.getUriForFile( context, authority, customHelp); i.setDataAndType(contentUri, "text/html"); + + // Grant read permission to all the files in the package so embedded assets can be viewed + ClipData clipData = new ClipData(null, + new String[] { + ClipDescription.MIMETYPE_TEXT_HTML, + "text/css", + "image/gif", + "image/jpeg", + "image/png" + }, new ClipData.Item(contentUri)); + + // Exclude html help files and JS files. Treat rest of the files as assets + FileFilter _fileFilter = new FileFilter() { + @Override + public boolean accept(File pathname) { + String name = pathname.getName(); + if (pathname.isFile() && (FileUtils.isReadmeFile(name) || + FileUtils.isWelcomeFile(name) || FileUtils.hasJavaScriptExtension(name))) { + return false; + } + return true; + } + }; + + File packageDir = new File( + context.getDir("data", Context.MODE_PRIVATE), "packages" + File.separator + packageID + File.separator); + File[] files = packageDir.listFiles(_fileFilter); + for(File assetFile : files) { + Uri assetUri = FileProvider.getUriForFile( + context, authority, assetFile); + clipData.addItem(new ClipData.Item(assetUri)); + } + + // Associate assets in clipData to the intent + i.setClipData(clipData); } catch (NullPointerException e) { String message = "FileProvider undefined in app to load" + customHelp.toString(); Toast.makeText(context, message, Toast.LENGTH_LONG).show(); From 47a2a5ec5f2ca489c501ec7960a537886b2878a3 Mon Sep 17 00:00:00 2001 From: jahorton Date: Fri, 20 Dec 2019 11:50:09 +0700 Subject: [PATCH 16/26] feat(ios): ios-phone predictive banner dark styling --- web/source/resources/osk/kmwosk.css | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/web/source/resources/osk/kmwosk.css b/web/source/resources/osk/kmwosk.css index cd59ff5232..b6ad0f0ede 100644 --- a/web/source/resources/osk/kmwosk.css +++ b/web/source/resources/osk/kmwosk.css @@ -96,6 +96,22 @@ color: #000; } +/* Probably best to make this its own CSS that can be optionally included? */ +@media (prefers-color-scheme: dark) { + .ios .kmw-banner-bar { + background-color: #0f1319; + } + + .ios .kmw-banner-bar .kmw-suggest-option { + background-color: #3d3d3e; + border-bottom: solid 1px #8a8d90; + } + + .ios .kmw-suggestion-text { + color: #fff; + } +} + .phone.ios .kmw-suggest-option.kmw-suggest-touched {background-color:#88f;} .phone.ios.kmw-osk-frame, From c772a2dbfef87a7c58d21a49d2a5a8edd0342fc3 Mon Sep 17 00:00:00 2001 From: Darcy Wong Date: Fri, 20 Dec 2019 15:35:28 +0700 Subject: [PATCH 17/26] chore(android): Refactor HelpFile as utility --- .../kmea/KeyboardInfoActivity.java | 63 +------------ .../kmea/KeyboardSettingsActivity.java | 27 ++---- .../com/tavultesoft/kmea/util/HelpFile.java | 89 +++++++++++++++++++ 3 files changed, 99 insertions(+), 80 deletions(-) create mode 100644 android/KMEA/app/src/main/java/com/tavultesoft/kmea/util/HelpFile.java diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KeyboardInfoActivity.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KeyboardInfoActivity.java index a6fd61f124..84eccb725f 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KeyboardInfoActivity.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KeyboardInfoActivity.java @@ -4,16 +4,11 @@ package com.tavultesoft.kmea; -import java.io.File; -import java.io.FileFilter; import java.util.ArrayList; import java.util.HashMap; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; -import androidx.core.content.FileProvider; -import android.content.ClipData; -import android.content.ClipDescription; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; @@ -33,8 +28,8 @@ import android.widget.SimpleAdapter; import android.widget.TextView; import android.widget.Toast; -import com.tavultesoft.kmea.util.FileUtils; import com.tavultesoft.kmea.util.FileProviderUtils; +import com.tavultesoft.kmea.util.HelpFile; import com.tavultesoft.kmea.util.MapCompat; import com.tavultesoft.kmea.util.QRCodeUtil; @@ -128,67 +123,15 @@ public final class KeyboardInfoActivity extends AppCompatActivity { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { if (position == 1) { - Intent i = new Intent(Intent.ACTION_VIEW); - if (customHelpLink != null) { // Display local welcome.htm help file, including associated assets - if (FileUtils.isWelcomeFile(customHelpLink) && ! KMManager.isTestMode()) { - File customHelp = new File(new File(customHelpLink).getAbsolutePath()); - i.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - // Starting with Android N, you can't pass file:// to intents, so we use FileProvider - try { - final String authority = FileProviderUtils.getAuthority(context); - Uri contentUri = FileProvider.getUriForFile( - context, authority, customHelp); - i.setDataAndType(contentUri, "text/html"); + Intent i = HelpFile.toActionView(context, customHelpLink, packageID); - // Grant read permission to all the files in the package so embedded assets can be viewed - ClipData clipData = new ClipData(null, - new String[] { - ClipDescription.MIMETYPE_TEXT_HTML, - "text/css", - "image/gif", - "image/jpeg", - "image/png" - }, new ClipData.Item(contentUri)); - - // Exclude html help files and JS files. Treat rest of the files as assets - FileFilter _fileFilter = new FileFilter() { - @Override - public boolean accept(File pathname) { - String name = pathname.getName(); - if (pathname.isFile() && (FileUtils.isReadmeFile(name) || - FileUtils.isWelcomeFile(name) || FileUtils.hasJavaScriptExtension(name))) { - return false; - } - return true; - } - }; - - File packageDir = new File( - context.getDir("data", Context.MODE_PRIVATE), "packages" + File.separator + packageID + File.separator); - File[] files = packageDir.listFiles(_fileFilter); - for(File assetFile : files) { - Uri assetUri = FileProvider.getUriForFile( - context, authority, assetFile); - clipData.addItem(new ClipData.Item(assetUri)); - } - - // Associate assets in clipData to the intent - i.setClipData(clipData); - } catch (NullPointerException e) { - String message = "FileProvider undefined in app to load" + customHelp.toString(); - Toast.makeText(context, message, Toast.LENGTH_LONG).show(); - Log.e("KeyboardInfoActivity", message); - } - } - else { - i.setData(Uri.parse(customHelpLink)); - } if (FileProviderUtils.exists(context)|| KMManager.isTestMode()) { startActivity(i); } } else { + Intent i = new Intent(Intent.ACTION_VIEW); i.setData(Uri.parse(helpUrlStr)); startActivity(i); } diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KeyboardSettingsActivity.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KeyboardSettingsActivity.java index 2e9ed67fd5..6921b86dcb 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KeyboardSettingsActivity.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KeyboardSettingsActivity.java @@ -33,6 +33,7 @@ import android.widget.Toast; import com.tavultesoft.kmea.util.FileUtils; import com.tavultesoft.kmea.util.FileProviderUtils; +import com.tavultesoft.kmea.util.HelpFile; import com.tavultesoft.kmea.util.MapCompat; import com.tavultesoft.kmea.util.QRCodeUtil; @@ -140,33 +141,19 @@ public final class KeyboardSettingsActivity extends AppCompatActivity { // "Help" link clicked if (itemTitle.equals(getString(R.string.help_link))) { - Intent i = new Intent(Intent.ACTION_VIEW); - if (customHelpLink != null) { - if (FileUtils.isWelcomeFile(customHelpLink)) { - File customHelp = new File(new File(customHelpLink).getAbsolutePath()); - i.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - // Starting with Android N, you can't pass file:// to intents, so we use FileProvider - try { - Uri contentUri = FileProvider.getUriForFile( - context, authority, customHelp); - i.setDataAndType(contentUri, "text/html"); - } catch (NullPointerException e) { - String message = "FileProvider undefined in app to load" + customHelp.toString(); - Toast.makeText(context, message, Toast.LENGTH_LONG).show(); - Log.e("TAG", message); - } - } - else { - i.setData(Uri.parse(customHelpLink)); - } - if (FileProviderUtils.exists(context)) { + // Display local welcome.htm help file, including associated assets + Intent i = HelpFile.toActionView(context, customHelpLink, packageID); + + if (FileProviderUtils.exists(context) || KMManager.isTestMode()) { startActivity(i); } } else { + Intent i = new Intent(Intent.ACTION_VIEW); i.setData(Uri.parse(helpUrlStr)); startActivity(i); } + // "Uninstall Keyboard" clicked } else if (itemTitle.equals(getString(R.string.uninstall_keyboard))) { // Uninstall selected keyboard diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/util/HelpFile.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/util/HelpFile.java new file mode 100644 index 0000000000..47a1d27436 --- /dev/null +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/util/HelpFile.java @@ -0,0 +1,89 @@ +package com.tavultesoft.kmea.util; + +import android.content.ClipData; +import android.content.ClipDescription; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.util.Log; +import android.widget.Toast; + +import androidx.core.content.FileProvider; + +import com.tavultesoft.kmea.KMManager; +import com.tavultesoft.kmea.util.FileProviderUtils; +import com.tavultesoft.kmea.util.FileUtils; + +import java.io.File; +import java.io.FileFilter; + + +public final class HelpFile { + private static final String TAG = "HelpFile"; + private static final String[] ASSET_MIME_TYPES = { + ClipDescription.MIMETYPE_TEXT_HTML, + "text/css", + "image/gif", + "image/jpeg", + "image/png"}; + + /** + * Utility to pass a help file and all associated assets to an Intent for Intent.ACTION_VIEW + * @param context + * @param helpFile Full path string of the html file to view + * @param packageID String of the package ID + * @return Intent + */ + public static Intent toActionView(Context context, String helpFile, String packageID) { + Intent i = new Intent(Intent.ACTION_VIEW); + + if (FileUtils.isWelcomeFile(helpFile) && ! KMManager.isTestMode()) { + File customHelp = new File(new File(helpFile).getAbsolutePath()); + i.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + // Starting with Android N, you can't pass file:// to intents, so we use FileProvider + try { + final String authority = FileProviderUtils.getAuthority(context); + Uri contentUri = FileProvider.getUriForFile( + context, authority, customHelp); + i.setDataAndType(contentUri, "text/html"); + + // Grant read permission to all the files in the package so embedded assets can be viewed + ClipData clipData = new ClipData(null, ASSET_MIME_TYPES, new ClipData.Item(contentUri)); + + // Exclude html help files and JS files. Treat rest of the files as assets + FileFilter _fileFilter = new FileFilter() { + @Override + public boolean accept(File pathname) { + String name = pathname.getName(); + if (pathname.isFile() && (FileUtils.isReadmeFile(name) || + FileUtils.isWelcomeFile(name) || FileUtils.hasJavaScriptExtension(name))) { + return false; + } + return true; + } + }; + + String base = helpFile.contains("packages") ? "packages" : "models"; + File packageDir = new File( + context.getDir("data", Context.MODE_PRIVATE), base + File.separator + packageID + File.separator); + File[] files = packageDir.listFiles(_fileFilter); + for(File assetFile : files) { + Uri assetUri = FileProvider.getUriForFile( + context, authority, assetFile); + clipData.addItem(new ClipData.Item(assetUri)); + } + + // Associate assets in clipData to the intent + i.setClipData(clipData); + } catch (NullPointerException e) { + String message = "FileProvider undefined in app to load" + customHelp.toString(); + Toast.makeText(context, message, Toast.LENGTH_LONG).show(); + Log.e(TAG, message); + } + } else { + i.setData(Uri.parse(helpFile)); + } + return i; + } + +} \ No newline at end of file From 66f86a904cec546ac888942f396227cc8ce38aba Mon Sep 17 00:00:00 2001 From: Darcy Wong Date: Fri, 20 Dec 2019 15:57:22 +0700 Subject: [PATCH 18/26] fix(android): Also use HelpFile for Model custom help link --- .../tavultesoft/kmea/ModelInfoActivity.java | 28 +++++-------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/ModelInfoActivity.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/ModelInfoActivity.java index 8f78d3a359..b360dbda6f 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/ModelInfoActivity.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/ModelInfoActivity.java @@ -28,6 +28,7 @@ import android.widget.Toast; import com.tavultesoft.kmea.util.FileUtils; import com.tavultesoft.kmea.util.FileProviderUtils; +import com.tavultesoft.kmea.util.HelpFile; import com.tavultesoft.kmea.util.MapCompat; import static com.tavultesoft.kmea.ConfirmDialogFragment.DialogType.DIALOG_TYPE_DELETE_MODEL; @@ -81,7 +82,7 @@ public final class ModelInfoActivity extends AppCompatActivity { // Currently, model help only available if custom link exists String icon = String.valueOf(R.drawable.ic_arrow_forward); // Don't show help link arrow if both custom help and File Provider don't exist - // TODO: Update this when model help available on help.keyman.com + // TODO: Update this when model help available on help.keyman.com if ( (!customHelpLink.equals("") && !FileProviderUtils.exists(context)) || customHelpLink.equals("") ){ icon = noIcon; @@ -128,32 +129,17 @@ public final class ModelInfoActivity extends AppCompatActivity { // "Help" link clicked if (itemTitle.equals(getString(R.string.help_link))) { - Intent i = new Intent(Intent.ACTION_VIEW); - if (!customHelpLink.equals("")) { - if (FileUtils.isWelcomeFile(customHelpLink)) { - File customHelp = new File(new File(customHelpLink).getAbsolutePath()); - i.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - // Starting with Android N, you can't pass file:// to intents, so we use FileProvider - try { - Uri contentUri = FileProvider.getUriForFile( - context, authority, customHelp); - i.setDataAndType(contentUri, "text/html"); - } catch (NullPointerException e) { - String message = "FileProvider undefined in app to load" + customHelp.toString(); - Toast.makeText(context, message, Toast.LENGTH_LONG).show(); - Log.e("ModelInfoActivity", message); - } - } - else { - i.setData(Uri.parse(customHelpLink)); - } - if (FileProviderUtils.exists(context)) { + // Display local welcome.htm help file, including associated assets + Intent i = HelpFile.toActionView(context, customHelpLink, packageID); + + if (FileProviderUtils.exists(context) || KMManager.isTestMode()) { startActivity(i); } } else { // We should always have a help file packaged with models. } + // "Uninstall Model" clicked } else if (itemTitle.equals(getString(R.string.uninstall_model))) { // Uninstall selected model From c0ac3166c64f9457276088e85d154893581a385e Mon Sep 17 00:00:00 2001 From: jahorton Date: Mon, 6 Jan 2020 09:39:01 +0700 Subject: [PATCH 19/26] feat(web): Dark mode CSS for ios phone form factors --- web/source/kmwnative.ts | 8 +++++-- web/source/kmwutils.ts | 10 ++++++++ web/source/resources/osk/kmwosk.css | 37 +++++++++++++++++++++++------ 3 files changed, 46 insertions(+), 9 deletions(-) diff --git a/web/source/kmwnative.ts b/web/source/kmwnative.ts index 66b6a7ac8b..3ccfda2985 100644 --- a/web/source/kmwnative.ts +++ b/web/source/kmwnative.ts @@ -204,7 +204,8 @@ namespace com.keyman.osk { * @param {number} edge -1 left edge, 1 right edge, else 0 */ VisualKeyboard.prototype.drawPreview = function(this: VisualKeyboard, canvas: HTMLCanvasElement, w: number, h: number, edge: number) { - let device = com.keyman.singleton.util.device; + let util = com.keyman.singleton.util; + let device = util.device; var ctx = canvas.getContext('2d'), dx = (canvas.width - w)/2, hMax = canvas.height, w0 = 0, w1 = dx, w2 = w + dx, w3 = w + 2 * dx, @@ -235,7 +236,10 @@ namespace com.keyman.osk { w1 = w2 = wx; ctx.fillStyle = '#999'; } else { - ctx.fillStyle = '#ffffff'; + // #0f1319 is the current dark-mode background color set in kmwosk.css. + // There might be a way to automatically retrieve it, but that'll take a + // bit of research to find. + ctx.fillStyle = util.prefersDarkMode() ? '#0f1319' : '#ffffff'; } ctx.lineWidth = 1; ctx.strokeStyle = '#cccccc'; diff --git a/web/source/kmwutils.ts b/web/source/kmwutils.ts index 42507e0214..f5bfcd2e12 100644 --- a/web/source/kmwutils.ts +++ b/web/source/kmwutils.ts @@ -1211,6 +1211,16 @@ namespace com.keyman { return this.checkFont(fd); } + + /** + * Checks is a user's browser is in dark mode, if the feature is supported. Returns false otherwise. + * + * Thanks to https://stackoverflow.com/a/57795518 for this code. + */ + prefersDarkMode(): boolean { + // Ensure the detector exists (otherwise, returns false) + return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; + } } } diff --git a/web/source/resources/osk/kmwosk.css b/web/source/resources/osk/kmwosk.css index b6ad0f0ede..dfd6df5b04 100644 --- a/web/source/resources/osk/kmwosk.css +++ b/web/source/resources/osk/kmwosk.css @@ -96,6 +96,13 @@ color: #000; } +.phone.ios .kmw-suggest-option.kmw-suggest-touched {background-color:#88f;} + +.phone.ios.kmw-osk-frame, +.tablet.ios.kmw-osk-frame { + background-color: #cfd3d9; +} + /* Probably best to make this its own CSS that can be optionally included? */ @media (prefers-color-scheme: dark) { .ios .kmw-banner-bar { @@ -110,15 +117,13 @@ .ios .kmw-suggestion-text { color: #fff; } + + .phone.ios.kmw-osk-frame, + .tablet.ios.kmw-osk-frame { + background-color: #0f1319; + } } -.phone.ios .kmw-suggest-option.kmw-suggest-touched {background-color:#88f;} - -.phone.ios.kmw-osk-frame, -.tablet.ios.kmw-osk-frame { - background-color: #cfd3d9; -} - .phone.android .kmw-key-layer-group {background-color: #333;} .phone.android .kmw-key {border: none; border-bottom: solid 1px #8a8d90; box-shadow:none; border-radius: 3px;} .phone.android .kmw-key.kmw-key-default {color:#fff;background-color:#777;} @@ -302,6 +307,18 @@ div.ios div.kmw-keytip { position:fixed; left:0; top:0; width:3em; height:3em; background-color:rgb(0,0,0,0);overflow:visible; } +/* Dark mode - ensure text is colored appropriately for key tips. */ +@media (prefers-color-scheme: dark) { + div.ios div.kmw-keytip { + color:#fff; + } + + /* Style for callout used on phones */ + #kmw-popup-callout { + background-color: #0f1319; + } +} + div.android div.kmw-keytip { position:fixed; left:0; top:0; width:3em; height:3em; color:#fff; @@ -364,6 +381,12 @@ div.android #keytip {background-color:#f00;} .phone.android #kmw-popup-keys {border:none; border-radius: 2px; background-color:#ccc; padding:5px 5px 0 0;} .tablet.android #kmw-popup-keys {border:1px solid #eee; border-radius: 3px; background-color:#888; padding:8px 12px 4px 4px;} +@media (prefers-color-scheme: dark) { + #kmw-popup-keys { + background-color: #0f1319; + } +} + /* Filter (shim) to darken screen and highlight popup keys */ #kmw-popup-shim { position: fixed; width: 100%; height: 100%; bottom: 0; left: 0; From a1b370177efd1b1a495941d2622746dddef8f4ba Mon Sep 17 00:00:00 2001 From: jahorton Date: Mon, 6 Jan 2020 09:43:35 +0700 Subject: [PATCH 20/26] feat(web): dark mode styling for ios tablets --- web/source/resources/osk/kmwosk.css | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/web/source/resources/osk/kmwosk.css b/web/source/resources/osk/kmwosk.css index dfd6df5b04..bc36d94fa0 100644 --- a/web/source/resources/osk/kmwosk.css +++ b/web/source/resources/osk/kmwosk.css @@ -187,6 +187,17 @@ .tablet.ios .kmw-suggest-option.kmw-suggest-touched {background-color:#88f;} +/* Probably best to make this its own CSS that can be optionally included? */ +@media (prefers-color-scheme: dark) { + .tablet.ios .kmw-key-layer-group {background-color: #0f1319} + .tablet.ios .kmw-key.kmw-key-default {color:#fff;background-color:#3d3d3e} + .tablet.ios .kmw-key.kmw-key-shift, + .tablet.ios .kmw-key.kmw-key-special {color:#fff;background-color:#595c62;} + .tablet.ios .kmw-key.kmw-key-shift-on, + .tablet.ios .kmw-key.kmw-key-special-on {color:#fff;background-color:#3d3d3e;} + .tablet.ios .kmw-key.kmw-key-touched {background-color:#447;} +} + .tablet .kmw-key-row {-webkit-touch-callout:none;-webkit-user-select:none;-ms-user-select:none;user-select:none;-webkit-tap-highlight-color:rgba(0,0,0,0);} .tablet.android .kmw-key-layer-group {background-color: #b4b4b8; border-top: 1px solid #666;} From addb6ee645ab09909a83a53f7bfb436a3aeb3c9f Mon Sep 17 00:00:00 2001 From: jahorton Date: Mon, 6 Jan 2020 15:18:06 +0700 Subject: [PATCH 21/26] fix(ios): fixes #2464, prevents in-menu keyboard display --- ios/engine/KMEI/KeymanEngine/Classes/Manager.swift | 7 +++++-- .../Classes/Settings/SettingsViewController.swift | 2 ++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift b/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift index 89757bd9b0..d89bed8ecf 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift @@ -976,9 +976,12 @@ public class Manager: NSObject, UIGestureRecognizerDelegate { // Keyboard download notification observers private func keyboardDownloadCompleted(_ keyboards: [InstallableKeyboard]) { - // TODO: Only do this if it's an update. We'll need a bit of notification retooling for this first. + // There's little harm in reloading the keyboard (and thus, KMW) for a clean reset + // after resource downloads or updates. That said, we should avoid *directly* + // triggering an immediate reset, as an extra reset will occur once we leave the + // settings menu. The delay also helps any chained downloads (keyboard > lexical model) + // to fully complete first. shouldReloadKeyboard = true - inputViewController.reload() } /*----------------------------- diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Settings/SettingsViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/Settings/SettingsViewController.swift index f7d207bb3c..e7550ac3cd 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Settings/SettingsViewController.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Settings/SettingsViewController.swift @@ -32,6 +32,8 @@ open class SettingsViewController: UITableViewController { } @objc func doneClicked(_ sender: Any) { + // While the called method might should be renamed, it does the job well enough. + // This resets KMW so that any new and/or updated resources can be properly loaded. Manager.shared.dismissKeyboardPicker(self) } From d5b38d12c0191a4d605ce378562dfe23b045b4e7 Mon Sep 17 00:00:00 2001 From: jahorton Date: Wed, 8 Jan 2020 09:42:44 +0700 Subject: [PATCH 22/26] 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 9e046b6ffb2716bb03bf64cd6666cbe65027913a Mon Sep 17 00:00:00 2001 From: jahorton Date: Wed, 8 Jan 2020 10:36:16 +0700 Subject: [PATCH 23/26] fix(web): Prevents infinite loop (w timeout) for ui init when embedded --- web/source/kmwdom.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/web/source/kmwdom.ts b/web/source/kmwdom.ts index ad4c05f533..370e663a98 100644 --- a/web/source/kmwdom.ts +++ b/web/source/kmwdom.ts @@ -1631,6 +1631,10 @@ namespace com.keyman { // Display the OSK (again) if enabled, in order to set its position correctly after // adding the UI to the page this.keyman.osk._Show(); + } else if(this.keyman.isEmbedded) { + // UI modules aren't utilized in embedded mode. There's nothing to init, so we simply + // return instead of waiting for a UI module that will never come. + return; } else { window.setTimeout(this.initializeUI.bind(this),1000); } From 0008e130985e344f8eb0bca207ebd2e4b32fdac6 Mon Sep 17 00:00:00 2001 From: Darcy Wong Date: Fri, 10 Jan 2020 08:35:09 +0700 Subject: [PATCH 24/26] chore(android): Update history.md Also add newline to end of HelpFile --- .../app/src/main/java/com/tavultesoft/kmea/util/HelpFile.java | 2 +- android/history.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/util/HelpFile.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/util/HelpFile.java index 47a1d27436..588a7546db 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/util/HelpFile.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/util/HelpFile.java @@ -86,4 +86,4 @@ public final class HelpFile { return i; } -} \ No newline at end of file +} diff --git a/android/history.md b/android/history.md index d571eb5214..c39b05497e 100644 --- a/android/history.md +++ b/android/history.md @@ -23,6 +23,7 @@ * Fix crashes when parsing JSON data from Cloud (#2393) * Improve compatibility with applications such as Gmail, Chrome that do not conform to the Android input APIs (#2382, #2376) * Propagate custom help links (#2448) + * Fix file permissions for viewing welcome.htm assets (#2465) ## 2019-12-12 12.0.4214 stable * Bug fix: From 7644f0d8e6ad843bc26af07f90a16549cc7daacb Mon Sep 17 00:00:00 2001 From: jahorton Date: Fri, 10 Jan 2020 08:50:12 +0700 Subject: [PATCH 25/26] 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) } } From b12fa22fa0c03a90731038509375e2b577c80161 Mon Sep 17 00:00:00 2001 From: jahorton Date: Fri, 10 Jan 2020 12:09:06 +0700 Subject: [PATCH 26/26] change(web): starts StyleConstants object for centralized style defs --- web/source/kmwdevice.ts | 30 ++++++++++++++++++++++++++++++ web/source/kmwnative.ts | 11 +++-------- web/source/kmwutils.ts | 10 ---------- web/source/utils/styleConstants.ts | 24 ++++++++++++++++++++++++ 4 files changed, 57 insertions(+), 18 deletions(-) create mode 100644 web/source/utils/styleConstants.ts diff --git a/web/source/kmwdevice.ts b/web/source/kmwdevice.ts index fce6196117..5588601aa4 100644 --- a/web/source/kmwdevice.ts +++ b/web/source/kmwdevice.ts @@ -1,5 +1,6 @@ // Includes version-related functionality /// +/// // The Device object definition ------------------------------------------------- @@ -13,6 +14,10 @@ namespace com.keyman { version: string; orientation: string|number; browser: string; + colorScheme: 'light' | 'dark'; + + private detected: boolean = false; + private _styles: utils.StyleConstants; // Generates a default Device value. constructor() { @@ -178,6 +183,9 @@ namespace com.keyman { } } } + + this.colorScheme = this.prefersDarkMode() ? 'dark' : 'light'; + this.detected = true; } static _GetIEVersion() { @@ -219,5 +227,27 @@ namespace com.keyman { return 999; } + + /** + * Checks is a user's browser is in dark mode, if the feature is supported. Returns false otherwise. + * + * Thanks to https://stackoverflow.com/a/57795518 for this code. + */ + private prefersDarkMode(): boolean { + // Ensure the detector exists (otherwise, returns false) + return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; + } + + public get styles(): utils.StyleConstants { + if(!this._styles) { + if(!this.detected) { + this.detect(); + } + + this._styles = new utils.StyleConstants(this); + } + + return this._styles; + } } } \ No newline at end of file diff --git a/web/source/kmwnative.ts b/web/source/kmwnative.ts index 3ccfda2985..cd3ad45a13 100644 --- a/web/source/kmwnative.ts +++ b/web/source/kmwnative.ts @@ -233,14 +233,9 @@ namespace com.keyman.osk { // Define appearance of preview (cannot be done directly in CSS) if(device.OS == 'Android') { var wx=(w1+w2)/2; - w1 = w2 = wx; - ctx.fillStyle = '#999'; - } else { - // #0f1319 is the current dark-mode background color set in kmwosk.css. - // There might be a way to automatically retrieve it, but that'll take a - // bit of research to find. - ctx.fillStyle = util.prefersDarkMode() ? '#0f1319' : '#ffffff'; - } + w1 = w2 = wx; + } + ctx.fillStyle = device.styles.popupCanvasBackgroundColor; ctx.lineWidth = 1; ctx.strokeStyle = '#cccccc'; diff --git a/web/source/kmwutils.ts b/web/source/kmwutils.ts index f5bfcd2e12..42507e0214 100644 --- a/web/source/kmwutils.ts +++ b/web/source/kmwutils.ts @@ -1211,16 +1211,6 @@ namespace com.keyman { return this.checkFont(fd); } - - /** - * Checks is a user's browser is in dark mode, if the feature is supported. Returns false otherwise. - * - * Thanks to https://stackoverflow.com/a/57795518 for this code. - */ - prefersDarkMode(): boolean { - // Ensure the detector exists (otherwise, returns false) - return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; - } } } diff --git a/web/source/utils/styleConstants.ts b/web/source/utils/styleConstants.ts new file mode 100644 index 0000000000..6c835fb580 --- /dev/null +++ b/web/source/utils/styleConstants.ts @@ -0,0 +1,24 @@ +// Includes Device definitions, which may play a role in constant logic. +/// + +/* + * This file is intended for CSS-styling constants that see use with the OSK. + */ + +namespace com.keyman.utils { + /** + * Defines device-level constants used for CSS styling. + */ + export class StyleConstants { + constructor(device: Device) { + // popupCanvasBackgroundColor + if(device.OS == 'Android') { + this.popupCanvasBackgroundColor = '#999'; + } else { + this.popupCanvasBackgroundColor = device.colorScheme == 'dark' ? '#0f1319' : '#ffffff'; + } + } + + public readonly popupCanvasBackgroundColor: string; + } +} \ No newline at end of file