chore(ios): Merge branch 'master' into ios-dark-mode-cntd

This commit is contained in:
jahorton 2020-01-13 15:53:24 +07:00
commit e91ec28ae6
26 changed files with 432 additions and 93 deletions

View file

@ -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";

View file

@ -4,7 +4,6 @@
package com.tavultesoft.kmea;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
@ -16,7 +15,6 @@ 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;
@ -30,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;
@ -52,7 +50,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);
@ -126,30 +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) {
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 {
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("KeyboardInfoActivity", message);
}
}
else {
i.setData(Uri.parse(customHelpLink));
}
// 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);
}

View file

@ -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

View file

@ -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

View file

@ -37,7 +37,7 @@ public class CloudDataJsonUtil {
public static HashMap<String,String> 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<String, String> keyboardInfo = new HashMap<String, String>();
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<String, String> hashMap = createKeyboardInfoMap(pkgID,langID,langName,kbID,kbName,kbVersion,isCustom,kbFont,null);
HashMap<String, String> hashMap = createKeyboardInfoMap(pkgID,langID,langName,kbID,kbName,kbVersion,isCustom,kbFont,null, customHelpLink);
// if (keyboardModifiedDates.get(kbID) == null) {

View file

@ -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)

View file

@ -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;
}

View file

@ -9,6 +9,7 @@ public interface LanguageResource {
String getLanguageName();
String getVersion();
String getPackage();
String getCustomHelpLink();
Bundle buildDownloadBundle();
}

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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<String, String> 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()

View file

@ -22,6 +22,8 @@
* Fix memory issues during build process (#2361)
* 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:

View file

@ -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
@ -976,9 +978,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()
}
/*-----------------------------

View file

@ -581,4 +581,8 @@ public class ResourceDownloadManager {
return updateQueue
}
public func installLexicalModelPackage(at packageURL: URL) -> InstallableLexicalModel? {
return downloader.installLexicalModelPackage(downloadedPackageFile: packageURL)
}
}

View file

@ -45,6 +45,12 @@ public class ResourceFileManager {
var destinationUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
destinationUrl.appendPathComponent(url.lastPathComponent)
// Since it's possible to request an install from a KMP in our owned document space,
// we need to check that it's not already in place where we want it.
if url == destinationUrl {
return url
}
do {
try copyWithOverwrite(from: url, to: destinationUrl)
return destinationUrl
@ -103,7 +109,9 @@ public class ResourceFileManager {
})
}
public func promptPackageInstall(of package: KeymanPackage, in rootVC: UIViewController) {
public func promptPackageInstall(of package: KeymanPackage,
in rootVC: UIViewController,
successHandler: ((KeymanPackage) -> Void)? = nil) {
let vc = PackageInstallViewController(for: package, completionHandler: { error in
if let err = error {
if let kmpError = err as? KMPError {
@ -111,7 +119,9 @@ public class ResourceFileManager {
rootVC.present(alert, animated: true, completion: nil)
}
} else {
let alert = self.buildSimpleAlert(title: "Success", message: "Installed successfully.")
let alert = self.buildSimpleAlert(title: "Success", message: "Installed successfully.", completionHandler: {
successHandler?(package)
})
rootVC.present(alert, animated: true, completion: nil)
}
})
@ -124,12 +134,14 @@ public class ResourceFileManager {
return buildSimpleAlert(title: "Error", message: error.rawValue)
}
public func buildSimpleAlert(title: String, message: String) -> UIAlertController {
public func buildSimpleAlert(title: String, message: String, completionHandler: (() -> Void)? = nil ) -> UIAlertController {
let alertController = UIAlertController(title: title, message: message,
preferredStyle: UIAlertController.Style.alert)
alertController.addAction(UIAlertAction(title: "OK",
style: UIAlertAction.Style.default,
handler: nil))
handler: { _ in
completionHandler?()
}))
//UIApplication.shared.keyWindow?.rootViewController?.present(alertController, animated: true, completion: nil)
return alertController

View file

@ -31,6 +31,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)
}
@ -69,6 +71,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
}
@ -84,8 +96,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 {
@ -148,6 +159,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
@ -187,6 +200,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
@ -207,7 +222,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
}

View file

@ -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)

View file

@ -80,7 +80,7 @@
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportsDocumentBrowser</key>
<false/>
<true/>
<key>UTExportedTypeDeclarations</key>
<array>
<dict>

View file

@ -136,6 +136,7 @@
CE6138021FB999C8009D0EF2 /* KeymanEngine.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE6138031FB999C8009D0EF2 /* KeymanEngine.framework */; };
CE79CDB52370111200010C06 /* Themes+Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CE79CDB42370111200010C06 /* Themes+Colors.xcassets */; };
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 */
@ -300,6 +301,7 @@
CE6138031FB999C8009D0EF2 /* KeymanEngine.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = KeymanEngine.framework; sourceTree = BUILT_PRODUCTS_DIR; };
CE79CDB42370111200010C06 /* Themes+Colors.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Themes+Colors.xcassets"; sourceTree = "<group>"; };
CE7C1AE1236925D800100C2C /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
CE7FF1EF239A0293007859D9 /* PackageBrowserViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackageBrowserViewController.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -524,6 +526,7 @@
9845A7C71A439A9200544E2E /* SetUpViewController */,
98ABADBA176935E400B62590 /* Supporting Files */,
C0FF769D1F5D4ECB00BD23C3 /* ActivityItemProvider.swift */,
CE7FF1EF239A0293007859D9 /* PackageBrowserViewController.swift */,
C0FF769C1F5D4ECA00BD23C3 /* Keyman-Bridging-Header.h */,
C0E943F71F61234C00E7D98C /* UIImage+Helpers.swift */,
C0E943F91F6124E100E7D98C /* AppDelegate.swift */,
@ -928,6 +931,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 */,

View file

@ -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) {

View file

@ -0,0 +1,78 @@
//
// PackageBrowserViewController.swift
// Keyman
//
// Created by Joshua Horton on 12/6/19.
// Copyright © 2019 SIL International. All rights reserved.
//
import UIKit
import SwiftUI
import KeymanEngine
@available(iOS 11.0, *)
class PackageBrowserViewController: UIDocumentBrowserViewController, UIDocumentBrowserViewControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
allowsDocumentCreation = false
allowsPickingMultipleItems = false
// Update the style of the UIDocumentBrowserViewController
// browserUserInterfaceStyle = .dark
// view.tintColor = .white
// Specify the allowed content types of your application via the Info.plist.
// Do any additional setup after loading the view.
}
// MARK: UIDocumentBrowserViewControllerDelegate
func documentBrowser(_ controller: UIDocumentBrowserViewController, didPickDocumentsAt documentURLs: [URL]) {
guard let sourceURL = documentURLs.first else { return }
// Present the Document View Controller for the first document that was picked.
// If you support picking multiple items, make sure you handle them all.
doInstall(of: sourceURL)
}
func documentBrowser(_ controller: UIDocumentBrowserViewController,
didImportDocumentAt sourceURL: URL,
toDestinationURL destinationURL: URL) {
// Present the Document View Controller for the new newly created document
doInstall(of: destinationURL)
}
func documentBrowser(_ controller: UIDocumentBrowserViewController,
failedToImportDocumentAt documentURL: URL,
error: Error?) {
// Make sure to handle the failed import appropriately, e.g., by presenting an error message to the user.
}
// MARK: Document Presentation
func doInstall(of url: URL) {
// Once selected, start the standard install process.
let rfm = ResourceFileManager.shared
guard let destinationUrl = rfm.importFile(url) else {
return
}
rfm.prepareKMPInstall(from: destinationUrl,
alertHost: self,
completionHandler: { package in
// We choose to prompt the user for comfirmation, rather
// than automatically installing the package.
rfm.promptPackageInstall(of: package, in: self, successHandler: { _ in
// Auto-dismiss the document browser upon successful KMP install.
// It's likely quite rare that someone would want to install 2+ at once.
self.navigationController?.popViewController(animated: true)
})
})
}
}

View file

@ -1,5 +1,6 @@
// Includes version-related functionality
///<reference path="utils/version.ts"/>
///<reference path="utils/styleConstants.ts" />
// 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;
}
}
}

View file

@ -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);
}

View file

@ -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,
@ -232,11 +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 {
ctx.fillStyle = '#ffffff';
}
w1 = w2 = wx;
}
ctx.fillStyle = device.styles.popupCanvasBackgroundColor;
ctx.lineWidth = 1;
ctx.strokeStyle = '#cccccc';

View file

@ -64,6 +64,17 @@
.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}
.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;
@ -90,7 +101,28 @@
.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 {
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-osk-frame,
.tablet.ios.kmw-osk-frame {
background-color: #0f1319;
}
}
.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;}
@ -155,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;}
@ -268,19 +311,31 @@ 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;
}
/* 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;
background-color: rgb(0,0,0,0);
overflow:visible;
}
}
div.android #keytip {background-color:#f00;}
@ -337,6 +392,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;
@ -358,6 +419,11 @@ 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);}
}
/* 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;}

View file

@ -0,0 +1,24 @@
// Includes Device definitions, which may play a role in constant logic.
///<reference path="../kmwdevice.ts" />
/*
* 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;
}
}