diff --git a/HISTORY.md b/HISTORY.md
index 1e121ddb86..8f7978032e 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -1,5 +1,14 @@
# Keyman Version History
+## 17.0.214 alpha 2023-11-20
+
+* fix(ios): fv: replace Zip framework to prevent crash on startup (#10018)
+
+## 17.0.213 alpha 2023-11-17
+
+* fix(linux): Fix packaging GHA (#10020)
+* fix(android): Always display HTML banner when suggestions aren't available (#9696)
+
## 17.0.212 alpha 2023-11-16
* chore(web): splits banner.ts into separate files per banner type (#9987)
diff --git a/VERSION.md b/VERSION.md
index 8bd573dbaf..87185aaedf 100644
--- a/VERSION.md
+++ b/VERSION.md
@@ -1 +1 @@
-17.0.213
\ No newline at end of file
+17.0.215
\ No newline at end of file
diff --git a/android/KMAPro/kMAPro/src/main/assets/banner.html b/android/KMAPro/kMAPro/src/main/assets/banner.html
new file mode 100644
index 0000000000..41107f1142
--- /dev/null
+++ b/android/KMAPro/kMAPro/src/main/assets/banner.html
@@ -0,0 +1,11 @@
+
+
+
+
+

+
+
+
+
+
+
diff --git a/android/KMAPro/kMAPro/src/main/assets/banner/keyman_banner.svg b/android/KMAPro/kMAPro/src/main/assets/banner/keyman_banner.svg
new file mode 100644
index 0000000000..3888a0937f
--- /dev/null
+++ b/android/KMAPro/kMAPro/src/main/assets/banner/keyman_banner.svg
@@ -0,0 +1,372 @@
+
+
diff --git a/android/KMAPro/kMAPro/src/main/java/com/keyman/android/BannerController.java b/android/KMAPro/kMAPro/src/main/java/com/keyman/android/BannerController.java
new file mode 100644
index 0000000000..f716faa9aa
--- /dev/null
+++ b/android/KMAPro/kMAPro/src/main/java/com/keyman/android/BannerController.java
@@ -0,0 +1,29 @@
+package com.keyman.android;
+
+import android.content.Context;
+
+import com.keyman.engine.KMManager;
+import com.keyman.engine.util.FileUtils;
+
+import java.io.File;
+
+public class BannerController {
+
+ // Paths relative to assets folder for banner themes
+ public static final String KM_BANNER_DIR = "banner";
+ public static final String KM_BANNER_THEME_KEYMAN = "banner.html";
+
+ public static void setHTMLBanner(Context context, KMManager.KeyboardType keyboardType) {
+ if (keyboardType == KMManager.KeyboardType.KEYBOARD_TYPE_UNDEFINED) {
+ return;
+ }
+
+ KMManager.copyHTMLBannerAssets(context, KM_BANNER_DIR);
+
+ // Always use Keyman banner theme
+ String contents = FileUtils.readContents(context, KM_BANNER_THEME_KEYMAN);
+ KMManager.setHTMLBanner(keyboardType, contents);
+ KMManager.setBanner(keyboardType, KMManager.BannerType.HTML);
+ KMManager.showBanner(true);
+ }
+}
diff --git a/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java b/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java
index 697825094b..cf919deddb 100644
--- a/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java
+++ b/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java
@@ -6,6 +6,7 @@ package com.keyman.android;
import com.tavultesoft.kmapro.BuildConfig;
import com.tavultesoft.kmapro.KeymanSettingsActivity;
+import com.keyman.android.BannerController;
import com.keyman.engine.KMManager;
import com.keyman.engine.KMManager.KeyboardType;
import com.keyman.engine.KMHardwareKeyboardInterpreter;
@@ -71,6 +72,9 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven
KMManager.SpacebarText spacebarText = KMManager.SpacebarText.fromString(prefs.getString(KeymanSettingsActivity.spacebarTextKey, KMManager.SpacebarText.LANGUAGE_KEYBOARD.toString()));
KMManager.setSpacebarText(spacebarText);
+ // Set the system keyboard HTML banner
+ BannerController.setHTMLBanner(this, KeyboardType.KEYBOARD_TYPE_SYSTEM);
+
boolean mayHaveHapticFeedback = prefs.getBoolean(KeymanSettingsActivity.hapticFeedbackKey, false);
KMManager.setHapticFeedback(mayHaveHapticFeedback);
@@ -93,6 +97,9 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven
@Override
public void onInitializeInterface() {
super.onInitializeInterface();
+
+ // KeymanWeb reloaded, so we have to pass the banner again
+ BannerController.setHTMLBanner(this, KeyboardType.KEYBOARD_TYPE_SYSTEM);
}
/**
diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java
index 6b92d4c2eb..16899f7c2d 100644
--- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java
+++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java
@@ -18,6 +18,7 @@ import java.util.List;
import java.util.Map;
import com.keyman.android.CheckInstallReferrer;
+import com.keyman.android.BannerController;
import com.keyman.engine.BaseActivity;
import com.keyman.engine.KMHelpFileActivity;
import com.keyman.engine.KMKeyboardDownloaderActivity;
@@ -499,6 +500,8 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene
@Override
public void onKeyboardShown() {
+ // Refresh banner theme
+ BannerController.setHTMLBanner(this, KeyboardType.KEYBOARD_TYPE_INAPP);
resizeTextView(true);
}
diff --git a/android/KMEA/app/src/main/assets/android-host.js b/android/KMEA/app/src/main/assets/android-host.js
index afa2825e33..7930c76f11 100644
--- a/android/KMEA/app/src/main/assets/android-host.js
+++ b/android/KMEA/app/src/main/assets/android-host.js
@@ -8,6 +8,9 @@ if(window.parent && window.parent.jsInterface && !window.jsInterface) {
var device = window.jsInterface.getDeviceType();
var oskHeight = Math.ceil(window.jsInterface.getKeyboardHeight() / window.devicePixelRatio);
var oskWidth = 0;
+var bannerHeight = 0;
+var bannerImagePath = '';
+var bannerHTMLContents = '';
var fragmentToggle = 0;
var sentryManager = new KeymanSentryManager({
@@ -37,11 +40,13 @@ function init() {
oninserttext: insertText,
root:'./'
}).then(function () { // Note: For non-upgraded API 21, arrow functions will break the keyboard!
- const bannerHeight = Math.ceil(window.jsInterface.getDefaultBannerHeight() / window.devicePixelRatio);
+ bannerHeight = Math.ceil(window.jsInterface.getDefaultBannerHeight() / window.devicePixelRatio);
+ if (bannerHeight > 0) {
- // The OSK is not available until initialization is complete.
- keyman.osk.bannerView.activeBannerHeight = bannerHeight;
- keyman.refreshOskLayout();
+ // The OSK is not available until initialization is complete.
+ keyman.osk.bannerView.activeBannerHeight = bannerHeight;
+ keyman.refreshOskLayout();
+ }
});
keyman.addEventListener('keyboardloaded', setIsChiral);
@@ -53,6 +58,29 @@ function init() {
notifyHost('pageLoaded');
}
+function showBanner(flag) {
+ console_debug("Setting banner display for dictionaryless keyboards to " + flag);
+ console_debug("bannerHTMLContents: " + bannerHTMLContents);
+ var bc = keyman.osk.bannerController;
+ if (bc) {
+ if (bannerHTMLContents != '') {
+ bc.inactiveBanner = flag ? new bc.HTMLBanner(bannerHTMLContents) : null;
+ } else {
+ bc.inactiveBanner = flag ? new bc.ImageBanner(bannerImagePath) : null;
+ }
+ }
+}
+
+function setBannerImage(path) {
+ bannerImagePath = path;
+}
+
+// Set the HTML banner to use when predictive-text is not available
+// contents - HTML content to use for the banner
+function setBannerHTML(contents) {
+ bannerHTMLContents = contents;
+}
+
function notifyHost(event, params) {
console_debug('notifyHost(event='+event+',params='+params+')');
// TODO: Update all other host notifications to use notifyHost instead of directly setting window.location.hash
@@ -65,12 +93,19 @@ function notifyHost(event, params) {
}
// Update the KMW banner height
+// h is in dpi (different from iOS)
function setBannerHeight(h) {
if (h > 0) {
- var osk = keyman.osk;
- osk.banner.height = Math.ceil(h / window.devicePixelRatio);
+ // The banner itself may not be loaded yet. This will preemptively help set
+ // its eventual display height.
+ bannerHeight = Math.ceil(h / window.devicePixelRatio);
+
+ if (keyman.osk) {
+ keyman.osk.bannerView.activeBannerHeight = bannerHeight;
+ }
}
- // Refresh KMW OSK
+
+ // Refresh KMW's OSK
keyman.refreshOskLayout();
}
diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java
index 6471e87510..215630cd03 100644
--- a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java
+++ b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java
@@ -7,13 +7,11 @@ package com.keyman.engine;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
-import java.util.List;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
-import com.keyman.engine.BaseActivity;
import com.keyman.engine.data.Keyboard;
import com.keyman.engine.data.KeyboardController;
import com.keyman.engine.KMManager.KeyboardType;
@@ -25,19 +23,11 @@ import com.keyman.engine.util.FileUtils;
import com.keyman.engine.util.KMLog;
import com.keyman.engine.util.KMString;
-import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
import android.content.res.Configuration;
-import android.graphics.Color;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.graphics.Typeface;
-import android.os.Build;
-import android.os.Bundle;
import android.os.Handler;
import android.util.DisplayMetrics;
import android.util.Log;
@@ -46,7 +36,6 @@ import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
-import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.inputmethod.ExtractedText;
import android.view.inputmethod.ExtractedTextRequest;
@@ -56,12 +45,9 @@ import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.widget.Button;
-import android.widget.FrameLayout;
-import android.widget.GridLayout;
import android.widget.PopupWindow;
import android.widget.PopupWindow.OnDismissListener;
import android.widget.RelativeLayout;
-import android.widget.TextView;
import android.widget.Toast;
import io.sentry.Breadcrumb;
@@ -84,19 +70,10 @@ final class KMKeyboard extends WebView {
private static String currentKeyboard = null;
- /**
- * Banner state value: "blank" - no banner available.
- */
- protected static final String KM_BANNER_STATE_BLANK = "blank";
- /**
- * Banner state value: "suggestion" - dictionary suggestions are shown.
- */
- protected static final String KM_BANNER_STATE_SUGGESTION = "suggestion";
-
/**
* Current banner state.
*/
- protected static String currentBanner = KM_BANNER_STATE_BLANK;
+ protected static KMManager.BannerType currentBanner = KMManager.BannerType.HTML;
private static String txtFont = "";
private static String oskFont = null;
@@ -105,6 +82,10 @@ final class KMKeyboard extends WebView {
private GestureDetector gestureDetector;
private static ArrayList kbEventListeners = null;
+ // Stores the current html string for use by the Banner
+ // when predictive text is not active
+ protected String htmlBannerString = "";
+
// Facilitates a 'lazy init' - we'll only check the preference when it matters,
// rather than at construction time.
private Boolean _shouldShowHelpBubble = null;
@@ -400,6 +381,9 @@ final class KMKeyboard extends WebView {
int bannerHeight = KMManager.getBannerHeight(context);
int oskHeight = KMManager.getKeyboardHeight(context);
+ if (this.htmlBannerString != null && !this.htmlBannerString.isEmpty()) {
+ setHTMLBanner(this.htmlBannerString);
+ }
loadJavascript(KMString.format("setBannerHeight(%d)", bannerHeight));
loadJavascript(KMString.format("setOskWidth(%d)", newConfig.screenWidthDp));
loadJavascript(KMString.format("setOskHeight(%d)", oskHeight));
@@ -425,22 +409,15 @@ final class KMKeyboard extends WebView {
return currentKeyboard;
}
- public static void setCurrentBanner(String banner) {
- currentBanner = banner;
- }
-
- public static String currentBanner() { return currentBanner; }
-
protected void toggleSuggestionBanner(HashMap associatedLexicalModel, boolean keyboardChanged) {
//reset banner state if new language has no lexical model
- if (currentBanner != null && currentBanner.equals(KM_BANNER_STATE_SUGGESTION)
+ if (currentBanner == KMManager.BannerType.SUGGESTION
&& associatedLexicalModel == null) {
- setCurrentBanner(KMKeyboard.KM_BANNER_STATE_BLANK);
+ currentBanner = KMManager.BannerType.HTML;
}
- if(keyboardChanged) {
- setLayoutParams(KMManager.getKeyboardLayoutParams());
- }
+ showBanner(true);
+ // Since there's always a banner, no need to update setLayoutParams()
}
/**
@@ -653,6 +630,30 @@ final class KMKeyboard extends WebView {
return retVal;
}
+ public void showBanner(boolean flag) {
+ String jsString = KMString.format("showBanner(%b)", flag);
+ loadJavascript(jsString);
+ }
+
+ public KMManager.BannerType getBanner() {
+ return currentBanner;
+ }
+
+ public void setBanner(KMManager.BannerType bannerType) {
+ currentBanner = bannerType;
+ }
+
+ public String getHTMLBanner() {
+ return this.htmlBannerString;
+ }
+
+ public void setHTMLBanner(String contents) {
+ this.htmlBannerString = contents;
+ String jsString = KMString.format("setBannerHTML(%s)",
+ JSONObject.quote(this.htmlBannerString));
+ loadJavascript(jsString);
+ }
+
public void setChirality(boolean flag) {
this.isChiral = flag;
}
diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardWebViewClient.java b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardWebViewClient.java
index e7188a94f9..4d458bd061 100644
--- a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardWebViewClient.java
+++ b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardWebViewClient.java
@@ -173,8 +173,7 @@ public final class KMKeyboardWebViewClient extends WebViewClient {
if (KMManager.currentLexicalModel != null) {
modelPredictionPref = prefs.getBoolean(KMManager.getLanguagePredictionPreferenceKey(KMManager.currentLexicalModel.get(KMManager.KMKey_LanguageID)), true);
}
- kmKeyboard.setCurrentBanner((isModelActive && modelPredictionPref) ?
- KMKeyboard.KM_BANNER_STATE_SUGGESTION : KMKeyboard.KM_BANNER_STATE_BLANK);
+ KMManager.setBannerOptions(isModelActive && modelPredictionPref);
RelativeLayout.LayoutParams params = KMManager.getKeyboardLayoutParams();
kmKeyboard.setLayoutParams(params);
} else if (url.indexOf("suggestPopup") >= 0) {
diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java b/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java
index cbcb28053a..df90f1d869 100644
--- a/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java
+++ b/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java
@@ -25,32 +25,21 @@ import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.content.res.Configuration;
-import android.graphics.Bitmap;
-import android.graphics.RectF;
import android.graphics.Typeface;
import android.inputmethodservice.InputMethodService;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
-import android.net.Uri;
import android.os.Build;
-import android.os.Handler;
import android.os.IBinder;
-import android.os.Looper;
import android.text.InputType;
import android.util.Log;
-import android.view.HapticFeedbackConstants;
-import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.inputmethod.EditorInfo;
-import android.view.inputmethod.ExtractedText;
-import android.view.inputmethod.ExtractedTextRequest;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
-import android.webkit.JavascriptInterface;
import android.webkit.WebView;
-import android.webkit.WebViewClient;
import android.widget.FrameLayout;
import android.widget.RelativeLayout;
@@ -71,7 +60,6 @@ import com.keyman.engine.packages.JSONUtils;
import com.keyman.engine.packages.LexicalModelPackageProcessor;
import com.keyman.engine.packages.PackageProcessor;
import com.keyman.engine.util.BCP47;
-import com.keyman.engine.util.CharSequenceUtil;
import com.keyman.engine.util.DependencyUtil;
import com.keyman.engine.util.DependencyUtil.LibraryType;
import com.keyman.engine.util.FileUtils;
@@ -157,6 +145,34 @@ public final class KMManager {
}
};
+ // Maps to enum BannerType in bannerView.ts
+ public enum BannerType {
+ BLANK,
+ IMAGE,
+ SUGGESTION,
+ HTML;
+
+ public static BannerType fromString(String mode) {
+ if (mode == null) return BLANK;
+ switch (mode) {
+ case "BLANK":
+ return BLANK;
+ case "image":
+ return IMAGE;
+ case "suggestion":
+ return SUGGESTION;
+ case "html":
+ return HTML;
+ }
+ return BLANK;
+ }
+
+ public String toString() {
+ String modes[] = { "blank", "image", "suggestion", "html"};
+ return modes[this.ordinal()];
+ }
+ }
+
protected static InputMethodService IMService;
private static boolean debugMode = false;
@@ -287,6 +303,9 @@ public final class KMManager {
public static final String KMFilename_LexicalModelsList = "lexical_models_list.dat";
+ public static final String KMBLACK_BANNER = "";
+ public static final String KMGRAY_BANNER = "";
+
private static Context appContext;
public static String getResourceRoot() {
@@ -636,6 +655,16 @@ public final class KMManager {
keyboard.addJavascriptInterface(new KMKeyboardJSHandler(appContext, keyboard), "jsInterface");
keyboard.loadKeyboard();
+ if (!isTestMode()) {
+ // For apps that don't specify an HTML banner, specify a default phone/tablet HTML banner
+ if (getFormFactor() == FormFactor.PHONE) {
+ keyboard.setHTMLBanner(KMBLACK_BANNER);
+ } else {
+ keyboard.setHTMLBanner(KMGRAY_BANNER);
+ }
+ keyboard.setBanner(KMManager.BannerType.HTML);
+ keyboard.showBanner(true);
+ }
setEngineWebViewVersionStatus(appContext, keyboard);
}
@@ -793,6 +822,31 @@ public final class KMManager {
return hasPermission(context, Manifest.permission.INTERNET);
}
+ /**
+ * Copy HTML banner assets to the app
+ * @param context - The context
+ * @param path - Folder relative to assets/ containing the banner file.
+ * @return boolean - true if assets copied
+ */
+ public static boolean copyHTMLBannerAssets(Context context, String path) {
+ AssetManager assetManager = context.getAssets();
+ try {
+ File bannerDir = new File(getResourceRoot() + File.separator + path);
+ if (!bannerDir.exists()) {
+ bannerDir.mkdir();
+ }
+
+ String[] bannerFiles = assetManager.list(path);
+ for (String bannerFile : bannerFiles) {
+ copyAsset(context, bannerFile, path, true);
+ }
+ return true;
+ } catch (Exception e) {
+ KMLog.LogException(TAG, "copyHTMLBannerAssets() failed. Error: ", e);
+ }
+ return false;
+ }
+
private static void copyAssets(Context context) {
AssetManager assetManager = context.getAssets();
try {
@@ -1370,7 +1424,13 @@ public final class KMManager {
KeyboardPickerActivity.deleteLexicalModel(context, position, silenceNotification);
}
- public static boolean setBannerOptions(boolean mayPredict) {
+ /**
+ * setBannerOptions - Update KMW whether to generate predictions.
+ * For now, also display banner
+ * @param mayPredict - boolean whether KMW should generate predictions
+ * @return boolean - Success
+ */
+ public static boolean setBannerOptions(boolean mayPredict) {
String url = KMString.format("setBannerOptions(%s)", mayPredict);
if (InAppKeyboard != null) {
InAppKeyboard.loadJavascript(url);
@@ -1379,6 +1439,73 @@ public final class KMManager {
if (SystemKeyboard != null) {
SystemKeyboard.loadJavascript(url);
}
+
+ return true;
+ }
+
+ /**
+ * Update KeymanWeb banner type
+ * @param {KeyboardType} keyboard
+ * @param {BannerType} bannerType
+ * @return status
+ */
+ public static boolean setBanner(KeyboardType keyboard, BannerType bannerType) {
+ if (keyboard == KeyboardType.KEYBOARD_TYPE_INAPP && InAppKeyboard != null) {
+ InAppKeyboard.setBanner(bannerType);
+ } else if (keyboard == KeyboardType.KEYBOARD_TYPE_SYSTEM && SystemKeyboard != null) {
+ SystemKeyboard.setBanner(bannerType);
+ } else {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Set the HTML content to use with the HTML banner
+ * @param {KeyboardType} keyboard
+ * @param {String} HTMl string
+ * @return {boolean}
+ */
+ public static boolean setHTMLBanner(KeyboardType keyboard, String htmlContent) {
+ if (keyboard == KeyboardType.KEYBOARD_TYPE_INAPP && InAppKeyboard != null) {
+ InAppKeyboard.setHTMLBanner(htmlContent);
+ } else if (keyboard == KeyboardType.KEYBOARD_TYPE_SYSTEM && SystemKeyboard != null) {
+ SystemKeyboard.setHTMLBanner(htmlContent);
+ } else {
+ Log.d(TAG, "setHTMLBanner() but keyboard is null");
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Get the HTML content associated with the HTML banner
+ * @param {KeyboardType} keyboard
+ * @return {String}
+ */
+ public static String getHTMLBanner(KeyboardType keyboard) {
+ if (keyboard == KeyboardType.KEYBOARD_TYPE_INAPP && InAppKeyboard != null) {
+ return InAppKeyboard.getHTMLBanner();
+ } else if (keyboard == KeyboardType.KEYBOARD_TYPE_SYSTEM && SystemKeyboard != null) {
+ return SystemKeyboard.getHTMLBanner();
+ }
+ return "";
+ }
+
+ /**
+ * showBanner - Update KMW whether to display banner.
+ * For now, always keep displaying banner
+ * @param flag - boolean whether KMW should display banner
+ * @return boolean - Success
+ */
+ public static boolean showBanner(boolean flag) {
+ if (InAppKeyboard != null) {
+ InAppKeyboard.showBanner(flag);
+ }
+
+ if (SystemKeyboard != null) {
+ SystemKeyboard.showBanner(flag);
+ }
return true;
}
@@ -1845,9 +1972,9 @@ public final class KMManager {
public static int getBannerHeight(Context context) {
int bannerHeight = 0;
- if (InAppKeyboard != null && InAppKeyboard.currentBanner().equals(KMKeyboard.KM_BANNER_STATE_SUGGESTION)) {
+ if (InAppKeyboard != null && InAppKeyboard.getBanner() != BannerType.BLANK) {
bannerHeight = (int) context.getResources().getDimension(R.dimen.banner_height);
- } else if (SystemKeyboard != null && SystemKeyboard.currentBanner().equals(KMKeyboard.KM_BANNER_STATE_SUGGESTION)) {
+ } else if (SystemKeyboard != null && SystemKeyboard.getBanner() != BannerType.BLANK) {
bannerHeight = (int) context.getResources().getDimension(R.dimen.banner_height);
}
return bannerHeight;
diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/util/FileUtils.java b/android/KMEA/app/src/main/java/com/keyman/engine/util/FileUtils.java
index d5610dedec..6b04b974f8 100644
--- a/android/KMEA/app/src/main/java/com/keyman/engine/util/FileUtils.java
+++ b/android/KMEA/app/src/main/java/com/keyman/engine/util/FileUtils.java
@@ -4,16 +4,22 @@
package com.keyman.engine.util;
import android.content.Context;
+import android.content.res.AssetManager;
+import android.util.Log;
+
+import com.keyman.engine.KMManager;
import org.json.JSONObject;
import org.json.JSONArray;
import java.io.BufferedInputStream;
+import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
@@ -248,6 +254,35 @@ public final class FileUtils {
return result;
}
+ /**
+ * Read the contents of asset file as a string
+ * Reference: https://stackoverflow.com/questions/16110002/read-assets-file-as-string
+ * @param context
+ * @param path - path of file relative to assets folder
+ * @return String
+ */
+ public static String readContents(Context context, String path) {
+ StringBuilder sb = new StringBuilder();
+ String str = "";
+ AssetManager assetManager = context.getAssets();
+ try {
+ InputStream inputStream = assetManager.open(path);
+ if (inputStream == null) {
+ KMLog.LogInfo(TAG, "Unable to read contents of asset: " + path);
+ return str;
+ }
+ BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
+ while ((str = reader.readLine()) != null) {
+ sb.append(str);
+ }
+ reader.close();
+ } catch (Exception e) {
+ KMLog.LogException(TAG, "Error reading asset file", e);
+ return str;
+ }
+ return sb.toString();
+ }
+
/**
* Utility to parse a URL and extract the filename
* @param urlStr String
diff --git a/core/tests/unit/ldml/ldml.cpp b/core/tests/unit/ldml/ldml.cpp
index 546af55ef4..e22367a7f9 100644
--- a/core/tests/unit/ldml/ldml.cpp
+++ b/core/tests/unit/ldml/ldml.cpp
@@ -128,16 +128,9 @@ apply_action(
assert(context.back().character == ch);
context.pop_back();
} else {
- assert(act.backspace.expected_type == KM_CORE_BT_UNKNOWN); // else it must be unknown
- // pop to first character or empty
- while (!context.empty() && context.back().type != KM_CORE_CT_CHAR) {
- // pop off all non-character entries
- context.pop_back();
- }
- if (!context.empty()) {
- // pop off the next character
- context.pop_back();
- }
+ // assume it's otherwise KM-coRE_BT_UNKNOWN
+ assert(act.backspace.expected_type == KM_CORE_BT_UNKNOWN);
+ assert(context.empty()); // if KM_CORE_BT_UNKNOWN, context should be empty.
}
}
break;
diff --git a/ios/Cartfile b/ios/Cartfile
index b265ef77af..57ec6de1e2 100644
--- a/ios/Cartfile
+++ b/ios/Cartfile
@@ -1,4 +1,4 @@
-github "marmelroy/Zip"
+github "weichsel/ZIPFoundation" ~> 0.9
github "keymanapp/dependency-XCGLogger" "master"
github "devicekit/DeviceKit" ~> 5.0
github "ashleymills/Reachability.swift"
diff --git a/ios/Cartfile.resolved b/ios/Cartfile.resolved
index ff30a8871d..5245b0dfa7 100644
--- a/ios/Cartfile.resolved
+++ b/ios/Cartfile.resolved
@@ -1,5 +1,5 @@
github "ashleymills/Reachability.swift" "v5.1.0"
-github "devicekit/DeviceKit" "5.0.0"
-github "getsentry/sentry-cocoa" "8.7.0"
+github "devicekit/DeviceKit" "5.1.0"
+github "getsentry/sentry-cocoa" "8.15.2"
github "keymanapp/dependency-XCGLogger" "57a7b975dbb6fe4fe90cef3d1bc52b8adbd89113"
-github "marmelroy/Zip" "2.1.2"
+github "weichsel/ZIPFoundation" "0.9.17"
diff --git a/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj b/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj
index f0205767d5..374f08bdcc 100644
--- a/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj
+++ b/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj
@@ -25,6 +25,7 @@
1645D5952036C6FF0076C51B /* KeymanPackage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1645D5942036C6FF0076C51B /* KeymanPackage.swift */; };
1645D5972036C9F80076C51B /* KMPKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1645D5962036C9F80076C51B /* KMPKeyboard.swift */; };
165EB3A12098993900040A69 /* KeyboardError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 165EB3A02098993900040A69 /* KeyboardError.swift */; };
+ 296EF2C72AFA26C700E3E384 /* ZIPFoundation.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 296EF2C62AFA26C700E3E384 /* ZIPFoundation.xcframework */; };
377D10DE26846B8900467431 /* SpacebarTextViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377D10DD26846B8900467431 /* SpacebarTextViewController.swift */; };
6CD5DFAA150F6DC8007A5DDE /* icon.png in Resources */ = {isa = PBXBuildFile; fileRef = 6CD5DFA8150F6DC8007A5DDE /* icon.png */; };
6CD5DFAB150F6DC8007A5DDE /* icon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 6CD5DFA9150F6DC8007A5DDE /* icon@2x.png */; };
@@ -121,7 +122,6 @@
CE5C8BE324B5B3BA00FAFB7F /* Queries+LexicalModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE5C8BE224B5B3BA00FAFB7F /* Queries+LexicalModel.swift */; };
CE5C8BE524B5BD1C00FAFB7F /* QueryModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE5C8BE424B5BD1C00FAFB7F /* QueryModelTests.swift */; };
CE5EDDC526522EAA001733AC /* XCGLogger.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE5EDDC026522EA9001733AC /* XCGLogger.xcframework */; };
- CE5EDDC626522EAA001733AC /* Zip.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE5EDDC126522EA9001733AC /* Zip.xcframework */; };
CE5EDDC726522EAA001733AC /* Sentry.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE5EDDC226522EAA001733AC /* Sentry.xcframework */; };
CE5EDDC826522EAA001733AC /* Reachability.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE5EDDC326522EAA001733AC /* Reachability.xcframework */; };
CE5EDDC926522EAA001733AC /* ObjcExceptionBridging.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE5EDDC426522EAA001733AC /* ObjcExceptionBridging.xcframework */; };
@@ -308,6 +308,7 @@
2949146F2738DA6700400732 /* ff-NG */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ff-NG"; path = "ff-NG.lproj/ResourceInfoView.strings"; sourceTree = ""; };
294914702738DD7400400732 /* ff-NG */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ff-NG"; path = "ff-NG.lproj/Localizable.strings"; sourceTree = ""; };
294914712738DD9700400732 /* ff-NG */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "ff-NG"; path = "ff-NG.lproj/Localizable.stringsdict"; sourceTree = ""; };
+ 296EF2C62AFA26C700E3E384 /* ZIPFoundation.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = ZIPFoundation.xcframework; path = ../../Carthage/Build/ZIPFoundation.xcframework; sourceTree = ""; };
297810FE297FAEDF007C886D /* kn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = kn; path = kn.lproj/ResourceInfoView.strings; sourceTree = ""; };
297810FF297FAEF8007C886D /* kn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = kn; path = kn.lproj/Localizable.strings; sourceTree = ""; };
29781100297FAF06007C886D /* kn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = kn; path = kn.lproj/Localizable.stringsdict; sourceTree = ""; };
@@ -436,7 +437,6 @@
CE5C8BE424B5BD1C00FAFB7F /* QueryModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueryModelTests.swift; sourceTree = ""; };
CE5EDDBB26522EA3001733AC /* DeviceKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = DeviceKit.xcframework; path = ../../Carthage/Build/DeviceKit.xcframework; sourceTree = ""; };
CE5EDDC026522EA9001733AC /* XCGLogger.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = XCGLogger.xcframework; path = ../../Carthage/Build/XCGLogger.xcframework; sourceTree = ""; };
- CE5EDDC126522EA9001733AC /* Zip.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Zip.xcframework; path = ../../Carthage/Build/Zip.xcframework; sourceTree = ""; };
CE5EDDC226522EAA001733AC /* Sentry.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Sentry.xcframework; path = ../../Carthage/Build/Sentry.xcframework; sourceTree = ""; };
CE5EDDC326522EAA001733AC /* Reachability.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Reachability.xcframework; path = ../../Carthage/Build/Reachability.xcframework; sourceTree = ""; };
CE5EDDC426522EAA001733AC /* ObjcExceptionBridging.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = ObjcExceptionBridging.xcframework; path = ../../Carthage/Build/ObjcExceptionBridging.xcframework; sourceTree = ""; };
@@ -560,8 +560,8 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ 296EF2C72AFA26C700E3E384 /* ZIPFoundation.xcframework in Frameworks */,
CE5EDDC526522EAA001733AC /* XCGLogger.xcframework in Frameworks */,
- CE5EDDC626522EAA001733AC /* Zip.xcframework in Frameworks */,
CE5EDDD52652372A001733AC /* DeviceKit.xcframework in Frameworks */,
CE5EDDC726522EAA001733AC /* Sentry.xcframework in Frameworks */,
CE5EDDC826522EAA001733AC /* Reachability.xcframework in Frameworks */,
@@ -949,11 +949,11 @@
F243888114BBD43000A3E055 /* Frameworks */ = {
isa = PBXGroup;
children = (
+ 296EF2C62AFA26C700E3E384 /* ZIPFoundation.xcframework */,
CE5EDDC426522EAA001733AC /* ObjcExceptionBridging.xcframework */,
CE5EDDC326522EAA001733AC /* Reachability.xcframework */,
CE5EDDC226522EAA001733AC /* Sentry.xcframework */,
CE5EDDC026522EA9001733AC /* XCGLogger.xcframework */,
- CE5EDDC126522EA9001733AC /* Zip.xcframework */,
CE5EDDBB26522EA3001733AC /* DeviceKit.xcframework */,
9A079DE52231A69D00581263 /* Foundation.framework */,
);
diff --git a/ios/engine/KMEI/KeymanEngine/Classes/KeymanPackage.swift b/ios/engine/KMEI/KeymanEngine/Classes/KeymanPackage.swift
index 28d89b7ee1..a13cef142c 100644
--- a/ios/engine/KMEI/KeymanEngine/Classes/KeymanPackage.swift
+++ b/ios/engine/KMEI/KeymanEngine/Classes/KeymanPackage.swift
@@ -7,7 +7,7 @@
//
import Foundation
-import Zip
+import ZIPFoundation
// KMPErrors may be passed to UIAlertControllers, so they need localization.
public enum KMPError : String, Error {
@@ -443,13 +443,28 @@ public class KeymanPackage {
@available(*, deprecated, message: "Use of the completion block is unnecessary; this method now returns synchronously.")
static public func extract(fileUrl: URL, destination: URL, complete: @escaping (KeymanPackage?) -> Void) throws {
- try unzipFile(fileUrl: fileUrl, destination: destination) {
- do {
- let package = try KeymanPackage.parse(destination)
- complete(package)
- } catch {
- SentryManager.captureAndLog(error, sentryLevel: .info)
- complete(nil)
+ let fileManager = FileManager()
+ do {
+ try fileManager.unzipItem(at: fileUrl, to: destination)
+ let package = try KeymanPackage.parse(destination)
+ complete(package)
+ } catch {
+ SentryManager.captureAndLog(error, sentryLevel: .info)
+ complete(nil)
+ }
+ }
+
+ static public func clearDirectory(destination: URL) throws {
+ // First check to see if directory exists. If not, then do nothing.
+ var isDirectory: ObjCBool = false
+ if(FileManager.default.fileExists(atPath: destination.path, isDirectory: &isDirectory)){
+ if (isDirectory.boolValue) {
+ // it exists and is actually a directory, so remove every file it contains
+ let fileArray = try FileManager.default.contentsOfDirectory(atPath: destination.path)
+ try fileArray.forEach { file in
+ let fileUrl = destination.appendingPathComponent(file)
+ try FileManager.default.removeItem(atPath: fileUrl.path)
+ }
}
}
}
@@ -460,7 +475,8 @@ public class KeymanPackage {
}
static public func unzipFile(fileUrl: URL, destination: URL, complete: @escaping () -> Void = {}) throws {
- try Zip.unzipFile(fileUrl, destination: destination, overwrite: true, password: nil)
+ let fileManager = FileManager()
+ try fileManager.unzipItem(at: fileUrl, to: destination)
complete()
}
diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift
index 11740ce42b..d3831413cb 100644
--- a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift
+++ b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift
@@ -183,6 +183,9 @@ public class ResourceFileManager {
var extractionFolder = cacheDirectory
extractionFolder.appendPathComponent("temp/\(archiveUrl.lastPathComponent)")
+ // first clear extraction folder to avoid creating duplicates
+ try KeymanPackage.clearDirectory(destination: extractionFolder)
+
do {
if let package = try KeymanPackage.extract(fileUrl: archiveUrl, destination: extractionFolder) {
return package
diff --git a/ios/engine/KMEI/KeymanEngineTests/KeymanPackageTests.swift b/ios/engine/KMEI/KeymanEngineTests/KeymanPackageTests.swift
index ee91cf96b9..baf61ee487 100644
--- a/ios/engine/KMEI/KeymanEngineTests/KeymanPackageTests.swift
+++ b/ios/engine/KMEI/KeymanEngineTests/KeymanPackageTests.swift
@@ -29,14 +29,13 @@ class KeymanPackageTests: XCTestCase {
}
}
- func testKeyboardPackageExtraction() throws {
+ func testKeyboardPackage_extractWithoutKmpExtension_succeeds() throws {
let cacheDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
- let khmerPackageZip = cacheDirectory.appendingPathComponent("khmer_angkor.kmp.zip")
+ let khmerPackageZip = cacheDirectory.appendingPathComponent("khmer_angkor.kmp")
try ResourceFileManager.shared.copyWithOverwrite(from: TestUtils.Keyboards.khmerAngkorKMP, to: khmerPackageZip)
let destinationFolderURL = cacheDirectory.appendingPathComponent("khmer_angkor")
- // Requires that the source file is already .zip, not .kmp. It's a ZipUtils limitation.
do {
if let kmp = try KeymanPackage.extract(fileUrl: khmerPackageZip, destination: destinationFolderURL) {
// Run assertions on the package's kmp.info.
@@ -58,14 +57,104 @@ class KeymanPackageTests: XCTestCase {
}
}
+ func testKeyboardPackage_clearNonexistentDirectory_doesNothing() throws {
+ let cacheDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
+ let destinationDirectory = cacheDirectory.appendingPathComponent("doesnotexist")
+
+ do {
+ // clear directory
+ try KeymanPackage.clearDirectory(destination: destinationDirectory)
+ } catch {
+ XCTFail("error clearing the nonexistent directory \(error)")
+ }
+ }
+
+ func testKeyboardPackage_clearEmptyDirectory_throwsNoError() throws {
+ let cacheDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
+ let destinationDirectory = cacheDirectory.appendingPathComponent("destination")
+
+ do {
+ // create directory
+ try FileManager.default.createDirectory(
+ atPath: destinationDirectory.path,
+ withIntermediateDirectories: false,
+ attributes: nil
+ )
+
+ // clear directory
+ try KeymanPackage.clearDirectory(destination: destinationDirectory)
+ } catch {
+ XCTFail("error clearing the empty directory \(error)")
+ }
+
+ let fileArray = try FileManager.default.contentsOfDirectory(atPath: destinationDirectory.path)
+ XCTAssert(fileArray.count == 0, "directory still contains \(fileArray.count) items")
+ }
+
+ func testKeyboardPackage_clearNonEmptyDirectory_directoryIsEmpty() throws {
+ let cacheDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
+ let destinationDirectory = cacheDirectory.appendingPathComponent("destination")
+
+ do {
+ // create directory
+ try FileManager.default.createDirectory(
+ atPath: destinationDirectory.path,
+ withIntermediateDirectories: false,
+ attributes: nil
+ )
+
+ // add some files
+ FileManager.default.createFile(atPath: destinationDirectory.appendingPathComponent("fileone").path, contents: nil)
+ FileManager.default.createFile(atPath: destinationDirectory.appendingPathComponent("filetwo").path, contents: nil)
+ FileManager.default.createFile(atPath: destinationDirectory.appendingPathComponent("filethree").path, contents: nil)
+
+ // clear directory
+ try KeymanPackage.clearDirectory(destination: destinationDirectory)
+ } catch {
+ XCTFail("error clearing the empty directory \(error)")
+ }
+
+ let fileArray = try FileManager.default.contentsOfDirectory(atPath: destinationDirectory.path)
+ XCTAssert(fileArray.count == 0, "directory still contains \(fileArray.count) items")
+ }
+
+ func testKeyboardPackage_extractTwice_noDuplicateFileError() throws {
+ let cacheDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
+ let khmerPackageZip = cacheDirectory.appendingPathComponent("khmer_angkor.kmp")
+ try ResourceFileManager.shared.copyWithOverwrite(from: TestUtils.Keyboards.khmerAngkorKMP, to: khmerPackageZip)
+
+ let destinationFolderURL = cacheDirectory.appendingPathComponent("khmer_angkor")
+
+ do {
+ if let kmp = try KeymanPackage.extract(fileUrl: khmerPackageZip, destination: destinationFolderURL) {
+ log.info("*** first unzip of \(kmp.id)")
+ do {
+ // clear directory before second extract
+ try KeymanPackage.clearDirectory(destination: destinationFolderURL)
+
+ if let secondKmp = try KeymanPackage.extract(fileUrl: khmerPackageZip, destination: destinationFolderURL) {
+ log.info("*** second unzip of \(secondKmp.id)")
+ } else {
+ XCTAssert(false, "*** second unzip failed")
+ }
+ } catch {
+ XCTFail("unzip 2 failure with error \(error)")
+ }
+ } else {
+ XCTAssert(false, "*** first unzip failed")
+ }
+ } catch {
+ XCTFail("unzip 1 failure with error \(error)")
+ }
+ }
+
func testLexicalModelPackageExtraction() throws {
let cacheDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
- let mtntZip = cacheDirectory.appendingPathComponent("nrc.en.mtnt.kmp.zip")
+ let mtntZip = cacheDirectory.appendingPathComponent("nrc.en.mtnt.kmp")
try ResourceFileManager.shared.copyWithOverwrite(from: TestUtils.LexicalModels.mtntKMP, to: mtntZip)
let destinationFolderURL = cacheDirectory.appendingPathComponent("nrc.en.mtnt.model")
- // Requires that the source file is already .zip, not .kmp. It's a ZipUtils limitation.
do {
if let kmp = try KeymanPackage.extract(fileUrl: mtntZip, destination: destinationFolderURL) {
// Run assertions on the package's kmp.info.
diff --git a/ios/engine/KMEI/KeymanEngineTests/TestUtils/EngineStateBundler.swift b/ios/engine/KMEI/KeymanEngineTests/TestUtils/EngineStateBundler.swift
index 1f3bd4e27c..b8950dc9c2 100644
--- a/ios/engine/KMEI/KeymanEngineTests/TestUtils/EngineStateBundler.swift
+++ b/ios/engine/KMEI/KeymanEngineTests/TestUtils/EngineStateBundler.swift
@@ -8,7 +8,8 @@
import Foundation
import XCTest
-import Zip
+import ZIPFoundation
+
@testable import KeymanEngine
extension TestUtils {
@@ -64,12 +65,17 @@ extension TestUtils {
try FileManager.default.moveItem(at: pListPath.appendingPathComponent(testEngineFilename), to: pListPath.appendingPathComponent(appGroupFilename))
}
- let attachmentFile = try Zip.quickZipFiles([bundleConstructionURL], fileName: "bundleArchive")
- log.info("Archive source: \(bundleConstructionURL)")
- let attachment = XCTAttachment(contentsOfFile: attachmentFile)
- attachment.lifetime = .keepAlways
-
- return attachment
- }
+ let archiveURL = bundleConstructionURL.appendingPathComponent("bundleArchive.zip")
+ do {
+ _ = try Archive(url: archiveURL, accessMode: .create)
+ log.info("archiveURL: \(archiveURL)")
+ let attachment = XCTAttachment(contentsOfFile: archiveURL)
+ attachment.lifetime = .keepAlways
+ return attachment
+ } catch let error {
+ print (error.localizedDescription)
+ return XCTAttachment(string: error.localizedDescription)
+ }
+ }
}
}
diff --git a/ios/keyman/Keyman/Keyman.xcodeproj/project.pbxproj b/ios/keyman/Keyman/Keyman.xcodeproj/project.pbxproj
index 5f2fad4ced..ddaa7a831c 100644
--- a/ios/keyman/Keyman/Keyman.xcodeproj/project.pbxproj
+++ b/ios/keyman/Keyman/Keyman.xcodeproj/project.pbxproj
@@ -14,6 +14,8 @@
162E2C9F2092D36800F40769 /* UIDevice+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 162E2C9E2092D36800F40769 /* UIDevice+Extensions.swift */; };
165EB39A2097165B00040A69 /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 165EB3992097165B00040A69 /* UIColor+Extensions.swift */; };
165EB39C2097181D00040A69 /* KMView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 165EB39B2097181D00040A69 /* KMView.swift */; };
+ 296EF2C42AFA267500E3E384 /* ZIPFoundation.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 296EF2C32AFA267500E3E384 /* ZIPFoundation.xcframework */; };
+ 296EF2C52AFA267500E3E384 /* ZIPFoundation.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 296EF2C32AFA267500E3E384 /* ZIPFoundation.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
981AFA8F19EF44DE006706BF /* 724-info@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 9815725518E4F0930014DF0C /* 724-info@2x.png */; };
981AFA9619EF44DE006706BF /* MainViewController_iPhone.xib in Resources */ = {isa = PBXBuildFile; fileRef = 98ABADCE176935E500B62590 /* MainViewController_iPhone.xib */; };
981AFA9819EF44DE006706BF /* 724-info.png in Resources */ = {isa = PBXBuildFile; fileRef = 9815725418E4F0930014DF0C /* 724-info.png */; };
@@ -155,8 +157,6 @@
CEBD34422654FEB400EB2EA8 /* Sentry.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CEBD34382654FEB400EB2EA8 /* Sentry.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
CEBD34432654FEB400EB2EA8 /* XCGLogger.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEBD34392654FEB400EB2EA8 /* XCGLogger.xcframework */; };
CEBD34442654FEB400EB2EA8 /* XCGLogger.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CEBD34392654FEB400EB2EA8 /* XCGLogger.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
- CEBD34452654FEB400EB2EA8 /* Zip.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEBD343A2654FEB400EB2EA8 /* Zip.xcframework */; };
- CEBD34462654FEB400EB2EA8 /* Zip.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CEBD343A2654FEB400EB2EA8 /* Zip.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
CEF4E55623E95B7B0065B9C7 /* ImageBanner.xib in Resources */ = {isa = PBXBuildFile; fileRef = CEF4E55523E95B7B0065B9C7 /* ImageBanner.xib */; };
CEF4E55723E95B7B0065B9C7 /* ImageBanner.xib in Resources */ = {isa = PBXBuildFile; fileRef = CEF4E55523E95B7B0065B9C7 /* ImageBanner.xib */; };
CEF4E55923E967140065B9C7 /* ImageBannerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF4E55823E967140065B9C7 /* ImageBannerViewController.swift */; };
@@ -183,11 +183,11 @@
files = (
CEBD34402654FEB400EB2EA8 /* Reachability.xcframework in Embed Frameworks */,
CEBD343E2654FEB400EB2EA8 /* ObjcExceptionBridging.xcframework in Embed Frameworks */,
+ 296EF2C52AFA267500E3E384 /* ZIPFoundation.xcframework in Embed Frameworks */,
CEBD343C2654FEB400EB2EA8 /* DeviceKit.xcframework in Embed Frameworks */,
CEBD34422654FEB400EB2EA8 /* Sentry.xcframework in Embed Frameworks */,
CEBD34442654FEB400EB2EA8 /* XCGLogger.xcframework in Embed Frameworks */,
CE80AD34257F2B4B008D2150 /* KeymanEngine.framework in Embed Frameworks */,
- CEBD34462654FEB400EB2EA8 /* Zip.xcframework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
@@ -205,6 +205,7 @@
16A9229D20325253003CC98E /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Keyman/Info.plist; sourceTree = SOURCE_ROOT; };
293EA3E027059C6900545EED /* ha */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ha; path = ha.lproj/Localizable.strings; sourceTree = ""; };
294914742738DF7700400732 /* ff-NG */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ff-NG"; path = "ff-NG.lproj/Localizable.strings"; sourceTree = ""; };
+ 296EF2C32AFA267500E3E384 /* ZIPFoundation.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = ZIPFoundation.xcframework; path = ../../Carthage/Build/ZIPFoundation.xcframework; sourceTree = ""; };
297810FD297FA818007C886D /* kn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = kn; path = kn.lproj/Localizable.strings; sourceTree = ""; };
298566B829802828004ACA95 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; };
298566C42980C493004ACA95 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Localizable.strings; sourceTree = ""; };
@@ -344,7 +345,6 @@
CEBD34372654FEB400EB2EA8 /* Reachability.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Reachability.xcframework; path = ../../Carthage/Build/Reachability.xcframework; sourceTree = ""; };
CEBD34382654FEB400EB2EA8 /* Sentry.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Sentry.xcframework; path = ../../Carthage/Build/Sentry.xcframework; sourceTree = ""; };
CEBD34392654FEB400EB2EA8 /* XCGLogger.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = XCGLogger.xcframework; path = ../../Carthage/Build/XCGLogger.xcframework; sourceTree = ""; };
- CEBD343A2654FEB400EB2EA8 /* Zip.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Zip.xcframework; path = ../../Carthage/Build/Zip.xcframework; sourceTree = ""; };
CEBD3458265511B700EB2EA8 /* am */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = am; path = am.lproj/Localizable.strings; sourceTree = ""; };
CEEC468226F2F7BA009A5B7D /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; };
CEEF81B92673019600EE6A07 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/Localizable.strings"; sourceTree = ""; };
@@ -359,11 +359,11 @@
files = (
CEBD343F2654FEB400EB2EA8 /* Reachability.xcframework in Frameworks */,
CEBD343D2654FEB400EB2EA8 /* ObjcExceptionBridging.xcframework in Frameworks */,
+ 296EF2C42AFA267500E3E384 /* ZIPFoundation.xcframework in Frameworks */,
CEBD343B2654FEB400EB2EA8 /* DeviceKit.xcframework in Frameworks */,
CEBD34412654FEB400EB2EA8 /* Sentry.xcframework in Frameworks */,
CEBD34432654FEB400EB2EA8 /* XCGLogger.xcframework in Frameworks */,
CE80AD33257F2B4A008D2150 /* KeymanEngine.framework in Frameworks */,
- CEBD34452654FEB400EB2EA8 /* Zip.xcframework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -544,12 +544,12 @@
98ABADB2176935E400B62590 /* Frameworks */ = {
isa = PBXGroup;
children = (
+ 296EF2C32AFA267500E3E384 /* ZIPFoundation.xcframework */,
CEBD34352654FEB400EB2EA8 /* DeviceKit.xcframework */,
CEBD34362654FEB400EB2EA8 /* ObjcExceptionBridging.xcframework */,
CEBD34372654FEB400EB2EA8 /* Reachability.xcframework */,
CEBD34382654FEB400EB2EA8 /* Sentry.xcframework */,
CEBD34392654FEB400EB2EA8 /* XCGLogger.xcframework */,
- CEBD343A2654FEB400EB2EA8 /* Zip.xcframework */,
CE80AD32257F2B4A008D2150 /* KeymanEngine.framework */,
);
name = Frameworks;
diff --git a/ios/keymanios.xcworkspace/xcshareddata/xcschemes/Keyman.xcscheme b/ios/keymanios.xcworkspace/xcshareddata/xcschemes/Keyman.xcscheme
index abc85ae142..8f86bea533 100644
--- a/ios/keymanios.xcworkspace/xcshareddata/xcschemes/Keyman.xcscheme
+++ b/ios/keymanios.xcworkspace/xcshareddata/xcschemes/Keyman.xcscheme
@@ -40,7 +40,8 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
- shouldUseLaunchSchemeArgsEnv = "NO">
+ shouldUseLaunchSchemeArgsEnv = "NO"
+ codeCoverageEnabled = "YES">
+
+
+
+

+
+
+
+
+
diff --git a/oem/firstvoices/android/app/src/main/assets/banner/red-logo.svg b/oem/firstvoices/android/app/src/main/assets/banner/red-logo.svg
new file mode 100644
index 0000000000..cf89f67db5
--- /dev/null
+++ b/oem/firstvoices/android/app/src/main/assets/banner/red-logo.svg
@@ -0,0 +1,38 @@
+
+
+
+
diff --git a/oem/firstvoices/android/app/src/main/java/com/firstvoices/android/BannerController.java b/oem/firstvoices/android/app/src/main/java/com/firstvoices/android/BannerController.java
new file mode 100644
index 0000000000..a0df15b2f1
--- /dev/null
+++ b/oem/firstvoices/android/app/src/main/java/com/firstvoices/android/BannerController.java
@@ -0,0 +1,29 @@
+package com.firstvoices.android;
+
+import android.content.Context;
+
+import com.keyman.engine.KMManager;
+import com.keyman.engine.util.FileUtils;
+
+import java.io.File;
+
+public class BannerController {
+
+ // Paths relative to assets folder for banner themes
+ public static final String FV_BANNER_DIR = "banner";
+ public static final String FV_BANNER_THEME = "banner.html";
+
+ public static void setHTMLBanner(Context context, KMManager.KeyboardType keyboardType) {
+ if (keyboardType == KMManager.KeyboardType.KEYBOARD_TYPE_UNDEFINED) {
+ return;
+ }
+
+ KMManager.copyHTMLBannerAssets(context, FV_BANNER_DIR);
+
+ // Always use FirstVoices banner theme
+ String contents = FileUtils.readContents(context, FV_BANNER_THEME);
+ KMManager.setHTMLBanner(keyboardType, contents);
+ KMManager.setBanner(keyboardType, KMManager.BannerType.HTML);
+ KMManager.showBanner(true);
+ }
+}
diff --git a/oem/firstvoices/android/app/src/main/java/com/firstvoices/keyboards/SystemKeyboard.java b/oem/firstvoices/android/app/src/main/java/com/firstvoices/keyboards/SystemKeyboard.java
index 95d0c51b37..0fb770c9b5 100644
--- a/oem/firstvoices/android/app/src/main/java/com/firstvoices/keyboards/SystemKeyboard.java
+++ b/oem/firstvoices/android/app/src/main/java/com/firstvoices/keyboards/SystemKeyboard.java
@@ -20,6 +20,7 @@ import android.view.inputmethod.ExtractedTextRequest;
import android.view.inputmethod.InputConnection;
import android.widget.FrameLayout;
+import com.firstvoices.android.BannerController;
import com.keyman.engine.KMManager;
import com.keyman.engine.KMManager.KeyboardType;
import com.keyman.engine.KMHardwareKeyboardInterpreter;
@@ -33,7 +34,6 @@ import io.sentry.android.core.SentryAndroid;
import io.sentry.Sentry;
public class SystemKeyboard extends InputMethodService implements OnKeyboardEventListener {
-
private View inputView = null;
private static ExtractedText exText = null;
private KMHardwareKeyboardInterpreter interpreter = null;
@@ -64,6 +64,9 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven
interpreter = new KMHardwareKeyboardInterpreter(getApplicationContext(), KeyboardType.KEYBOARD_TYPE_SYSTEM);
KMManager.setInputMethodService(this); // for HW interface
+
+ // Set the system keyboard HTML banner
+ BannerController.setHTMLBanner(this, KeyboardType.KEYBOARD_TYPE_SYSTEM);
}
@Override
@@ -79,7 +82,10 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven
* is called after creation and any configuration change. */
@Override
public void onInitializeInterface() {
- super.onInitializeInterface();
+ super.onInitializeInterface();
+
+ // KeymanWeb reloaded, so we have to pass the banner again
+ BannerController.setHTMLBanner(this, KeyboardType.KEYBOARD_TYPE_SYSTEM);
}
/** Called by the framework when your view for creating input needs to
diff --git a/oem/firstvoices/ios/Cartfile b/oem/firstvoices/ios/Cartfile
index 10efbc3d5a..57ec6de1e2 100644
--- a/oem/firstvoices/ios/Cartfile
+++ b/oem/firstvoices/ios/Cartfile
@@ -1,3 +1,4 @@
+github "weichsel/ZIPFoundation" ~> 0.9
github "keymanapp/dependency-XCGLogger" "master"
github "devicekit/DeviceKit" ~> 5.0
github "ashleymills/Reachability.swift"
diff --git a/oem/firstvoices/ios/Cartfile.resolved b/oem/firstvoices/ios/Cartfile.resolved
index 5ac3a55318..5245b0dfa7 100644
--- a/oem/firstvoices/ios/Cartfile.resolved
+++ b/oem/firstvoices/ios/Cartfile.resolved
@@ -1,4 +1,5 @@
github "ashleymills/Reachability.swift" "v5.1.0"
-github "devicekit/DeviceKit" "5.0.0"
-github "getsentry/sentry-cocoa" "8.7.0"
+github "devicekit/DeviceKit" "5.1.0"
+github "getsentry/sentry-cocoa" "8.15.2"
github "keymanapp/dependency-XCGLogger" "57a7b975dbb6fe4fe90cef3d1bc52b8adbd89113"
+github "weichsel/ZIPFoundation" "0.9.17"
diff --git a/oem/firstvoices/ios/FirstVoices.xcodeproj/project.pbxproj b/oem/firstvoices/ios/FirstVoices.xcodeproj/project.pbxproj
index 83e34c42a6..dcb81b684f 100644
--- a/oem/firstvoices/ios/FirstVoices.xcodeproj/project.pbxproj
+++ b/oem/firstvoices/ios/FirstVoices.xcodeproj/project.pbxproj
@@ -7,6 +7,9 @@
objects = {
/* Begin PBXBuildFile section */
+ 290FB4632AFB358F00249D58 /* ZIPFoundation.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 290FB4622AFB358E00249D58 /* ZIPFoundation.xcframework */; };
+ 290FB4642AFB358F00249D58 /* ZIPFoundation.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 290FB4622AFB358E00249D58 /* ZIPFoundation.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ 290FB4652AFB359A00249D58 /* ZIPFoundation.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 290FB4622AFB358E00249D58 /* ZIPFoundation.xcframework */; };
294D40E9279FE62600DB37F6 /* KeyboardRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 294D40E8279FE62600DB37F6 /* KeyboardRepository.swift */; };
294D40EC27A110BC00DB37F6 /* KeyboardSettingsRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 294D40EB27A110BC00DB37F6 /* KeyboardSettingsRepository.swift */; };
29694DC227A27EC300EA6C18 /* LexicalModelRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29694DC127A27EC300EA6C18 /* LexicalModelRepository.swift */; };
@@ -74,6 +77,7 @@
files = (
CEBD34202654D76900EB2EA8 /* Reachability.xcframework in Embed Frameworks */,
CEBD341E2654D76800EB2EA8 /* ObjcExceptionBridging.xcframework in Embed Frameworks */,
+ 290FB4642AFB358F00249D58 /* ZIPFoundation.xcframework in Embed Frameworks */,
CEBD341B2654D76600EB2EA8 /* DeviceKit.xcframework in Embed Frameworks */,
CEBD34232654D76B00EB2EA8 /* Sentry.xcframework in Embed Frameworks */,
CEBD34262654D76C00EB2EA8 /* XCGLogger.xcframework in Embed Frameworks */,
@@ -96,6 +100,7 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
+ 290FB4622AFB358E00249D58 /* ZIPFoundation.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = ZIPFoundation.xcframework; path = Carthage/Build/ZIPFoundation.xcframework; sourceTree = ""; };
294D40E8279FE62600DB37F6 /* KeyboardRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardRepository.swift; sourceTree = ""; };
294D40EB27A110BC00DB37F6 /* KeyboardSettingsRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardSettingsRepository.swift; sourceTree = ""; };
29694DC127A27EC300EA6C18 /* LexicalModelRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LexicalModelRepository.swift; sourceTree = ""; };
@@ -153,6 +158,7 @@
files = (
CEBD341D2654D76800EB2EA8 /* ObjcExceptionBridging.xcframework in Frameworks */,
CEBD341F2654D76900EB2EA8 /* Reachability.xcframework in Frameworks */,
+ 290FB4632AFB358F00249D58 /* ZIPFoundation.xcframework in Frameworks */,
CEBD34222654D76B00EB2EA8 /* Sentry.xcframework in Frameworks */,
CEBD341A2654D76600EB2EA8 /* DeviceKit.xcframework in Frameworks */,
CEBD34022654D41200EB2EA8 /* KeymanEngine.xcframework in Frameworks */,
@@ -166,6 +172,7 @@
files = (
CEDB327F265C9C58000A2009 /* DeviceKit.xcframework in Frameworks */,
CEDB3280265C9C58000A2009 /* ObjcExceptionBridging.xcframework in Frameworks */,
+ 290FB4652AFB359A00249D58 /* ZIPFoundation.xcframework in Frameworks */,
CEDB3281265C9C58000A2009 /* Reachability.xcframework in Frameworks */,
CEDB3282265C9C58000A2009 /* Sentry.xcframework in Frameworks */,
CEDB3283265C9C58000A2009 /* XCGLogger.xcframework in Frameworks */,
@@ -284,6 +291,7 @@
98C9A9FB1BFC19ED009E9A4F /* Frameworks */ = {
isa = PBXGroup;
children = (
+ 290FB4622AFB358E00249D58 /* ZIPFoundation.xcframework */,
CEBD34082654D5AA00EB2EA8 /* ObjcExceptionBridging.xcframework */,
CEBD340C2654D5AB00EB2EA8 /* Reachability.xcframework */,
CEBD34092654D5AA00EB2EA8 /* Sentry.xcframework */,