From 1f3fabda7845c986204932a5362c811bdd3ded82 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Thu, 25 Jun 2026 17:36:39 +0200 Subject: [PATCH 01/14] fix(android): fix loading of keyboards on Android This change moves to using `WebViewAssetLoader` for loading files from the device instead of using file:// URLs. This fixes the blank keyboard problem reported in #16096 for Android. Also make `KMKeyboard.getKeyboardRoot()` private, and rename public `Keyboard.getKeyboardPath()` to private `Keyboard.getKeyboardUrl()`. Part-of: #16096 --- android/KMAPro/kMAPro/build.gradle | 3 +- android/KMEA/app/build.gradle | 1 + .../KMEA/app/src/main/assets/keyboard.html | 2 +- .../java/com/keyman/engine/KMKeyboard.java | 40 +++++----- .../engine/KMKeyboardWebViewClient.java | 74 +++++++++++-------- .../java/com/keyman/engine/KMManager.java | 19 +++-- .../java/com/keyman/engine/data/Keyboard.java | 32 ++++---- .../com/keyman/engine/util/WebViewUtils.java | 8 ++ 8 files changed, 106 insertions(+), 73 deletions(-) diff --git a/android/KMAPro/kMAPro/build.gradle b/android/KMAPro/kMAPro/build.gradle index 458e78ba9ea..891bfda4470 100644 --- a/android/KMAPro/kMAPro/build.gradle +++ b/android/KMAPro/kMAPro/build.gradle @@ -172,8 +172,9 @@ dependencies { implementation 'com.stepstone.stepper:material-stepper:4.3.1' api(name: 'keyman-engine', ext: 'aar') implementation 'io.sentry:sentry-android:7.22.6' + implementation 'androidx.webkit:webkit:1.14.0' implementation 'androidx.preference:preference:1.2.1' - implementation "com.android.installreferrer:installreferrer:2.2" + implementation 'com.android.installreferrer:installreferrer:2.2' // Add dependency for generating QR Codes // (Even though it's embedded in KMEA, because we're manually copying keyman-engine.aar, diff --git a/android/KMEA/app/build.gradle b/android/KMEA/app/build.gradle index 3577463ca4b..50851a16679 100644 --- a/android/KMEA/app/build.gradle +++ b/android/KMEA/app/build.gradle @@ -75,6 +75,7 @@ dependencies { implementation 'commons-io:commons-io:2.16.1' implementation 'io.sentry:sentry-android:7.22.6' implementation 'androidx.preference:preference:1.2.1' + implementation 'androidx.webkit:webkit:1.14.0' // Robolectric testImplementation 'androidx.test.ext:junit:1.2.1' diff --git a/android/KMEA/app/src/main/assets/keyboard.html b/android/KMEA/app/src/main/assets/keyboard.html index 8d9d2d67dcc..720703d6e3b 100644 --- a/android/KMEA/app/src/main/assets/keyboard.html +++ b/android/KMEA/app/src/main/assets/keyboard.html @@ -1,7 +1,7 @@ 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 40611000020..eccead67bdc 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 @@ -1,5 +1,5 @@ -/** - * Copyright (C) 2017-2018 SIL International. All rights reserved. +/* + * Keyman is copyright (C) SIL Global. MIT License. */ package com.keyman.engine; @@ -23,6 +23,7 @@ import com.keyman.engine.util.FileUtils; import com.keyman.engine.util.KMLog; import com.keyman.engine.util.KMString; +import com.keyman.engine.util.WebViewUtils; import android.annotation.SuppressLint; import android.content.Context; @@ -85,7 +86,7 @@ final class KMKeyboard extends WebView { private static String txtFont = ""; private static String oskFont = null; - private static String keyboardRoot = ""; + private String keyboardRoot = ""; private final String fontUndefined = "undefined"; private GestureDetector gestureDetector; private static ArrayList kbEventListeners = null; @@ -367,7 +368,9 @@ public void loadKeyboard() { KMManager.SystemKeyboardWebViewClient.setKeyboardLoaded(false); } - String htmlPath = "file://" + getContext().getDir("data", Context.MODE_PRIVATE) + "/" + KMManager.KMFilename_KeyboardHtml; + // Use the reserved magic domain for loading the keyboard from the local device. + // See https://developer.android.com/reference/androidx/webkit/WebViewAssetLoader + String htmlPath = WebViewUtils.MAGIC_DEFAULT_DOMAIN + "/data/" + KMManager.KMFilename_KeyboardHtml; loadUrl(htmlPath); setBackgroundColor(0); } @@ -549,6 +552,7 @@ public static String oskFontFilename() { return oskFont; } + // REVIEW: this method seems to be unused /** * Return the full path to the special OSK font, * which is with all the keyboard assets at the root app_data folder @@ -703,7 +707,7 @@ public boolean setKeyboard(String packageID, String keyboardID, String languageI String kbKey = KMString.format("%s_%s", languageID, keyboardID); - String keyboardPath = makeKeyboardPath(packageID, keyboardID, keyboardVersion); + String keyboardUrl = makeKeyboardUrl(packageID, keyboardID, keyboardVersion); JSONObject reg = new JSONObject(); try { @@ -711,7 +715,7 @@ public boolean setKeyboard(String packageID, String keyboardID, String languageI reg.put("KI", "Keyboard_" + keyboardID); reg.put("KLC", languageID); reg.put("KL", languageName); - reg.put("KF", keyboardPath); + reg.put("KF", keyboardUrl); reg.put("KP", packageID); if (jDisplayFont != null) reg.put("KFont", jDisplayFont); @@ -799,26 +803,24 @@ private void sendError(String packageID, String keyboardID, String languageID, b // Set the base path of the keyboard depending on the package ID private void setKeyboardRoot(String packageID) { if (packageID.equals(KMManager.KMDefault_UndefinedPackageID)) { - this.keyboardRoot = (context.getDir("data", Context.MODE_PRIVATE).toString() + - File.separator + KMManager.KMDefault_UndefinedPackageID + File.separator); + this.keyboardRoot = WebViewUtils.MAGIC_DEFAULT_DOMAIN + "/data/" + KMManager.KMDefault_UndefinedPackageID + "/"; } else { - this.keyboardRoot = (context.getDir("data", Context.MODE_PRIVATE).toString() + - File.separator + KMManager.KMDefault_AssetPackages + File.separator + packageID + File.separator); + this.keyboardRoot = WebViewUtils.MAGIC_DEFAULT_DOMAIN + "/data/" + KMManager.KMDefault_AssetPackages + "/" + packageID + "/"; } } - public String getKeyboardRoot() { + private String getKeyboardRoot() { return this.keyboardRoot; } - private String makeKeyboardPath(String packageID, String keyboardID, String keyboardVersion) { - String keyboardPath; + private String makeKeyboardUrl(String packageID, String keyboardID, String keyboardVersion) { + String keyboardUrl = getKeyboardRoot(); if (packageID.equals(KMManager.KMDefault_UndefinedPackageID)) { - keyboardPath = getKeyboardRoot() + keyboardID + "-" + keyboardVersion + ".js"; + keyboardUrl += keyboardID + "-" + keyboardVersion + ".js"; } else { - keyboardPath = getKeyboardRoot() + keyboardID + ".js"; + keyboardUrl += keyboardID + ".js"; } - return keyboardPath; + return keyboardUrl; } private void sendKMWError(int lineNumber, String sourceId, String message) { @@ -1047,7 +1049,7 @@ private JSONObject makeFontPaths(String font) { JSONObject jfont = new JSONObject(); jfont.put(KMManager.KMKey_FontFamily, font.substring(0, font.length()-4)); JSONArray jfiles = new JSONArray(); - jfiles.put(keyboardRoot + font); + jfiles.put(getKeyboardRoot() + font); jfont.put(KMManager.KMKey_FontFiles, jfiles); return jfont; } @@ -1065,7 +1067,7 @@ private JSONObject makeFontPaths(String font) { Object obj = fontObj.get(KMManager.KMKey_FontFiles); if (obj instanceof String) { fontFile = fontObj.getString(KMManager.KMKey_FontFiles); - fontObj.put(KMManager.KMKey_FontFiles, keyboardRoot + obj); + fontObj.put(KMManager.KMKey_FontFiles, getKeyboardRoot() + obj); return fontObj; } else if (obj instanceof JSONArray) { sourceArray = fontObj.optJSONArray(KMManager.KMKey_FontFiles); @@ -1073,7 +1075,7 @@ private JSONObject makeFontPaths(String font) { for (int i = 0; i < sourceArray.length(); i++) { fontFile = sourceArray.getString(i); if (FileUtils.hasFontExtension(fontFile)) { - fontObj.put(KMManager.KMKey_FontFiles, keyboardRoot + fontFile); + fontObj.put(KMManager.KMKey_FontFiles, getKeyboardRoot() + fontFile); fontObj.remove(KMManager.KMKey_FontSource); return fontObj; } 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 853d997d7e2..c4a2818e6c6 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 @@ -1,5 +1,5 @@ -/** - * Copyright (C) 2023 SIL International. All rights reserved. +/* + * Keyman is copyright (C) SIL Global. MIT License. */ package com.keyman.engine; @@ -9,9 +9,13 @@ import android.graphics.RectF; import android.net.Uri; import android.util.Log; +import android.webkit.WebResourceRequest; +import android.webkit.WebResourceResponse; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.RelativeLayout; +import androidx.webkit.WebViewAssetLoader; +import androidx.webkit.WebViewAssetLoader.InternalStoragePathHandler; import com.keyman.engine.KeyboardEventHandler.EventType; import com.keyman.engine.KMManager; @@ -31,11 +35,16 @@ public final class KMKeyboardWebViewClient extends WebViewClient { public Context context; private KeyboardType keyboardType; private boolean keyboardLoaded; + private WebViewAssetLoader assetLoader; KMKeyboardWebViewClient(Context context, KeyboardType keyboardType) { this.context = context; this.keyboardType = keyboardType; this.keyboardLoaded = false; + this.assetLoader = new WebViewAssetLoader.Builder() + .addPathHandler("/data/", new InternalStoragePathHandler(context, + context.getDir("data", Context.MODE_PRIVATE))) + .build(); if (keyboardType != KeyboardType.KEYBOARD_TYPE_INAPP && keyboardType != KeyboardType.KEYBOARD_TYPE_SYSTEM) { KMLog.LogError(TAG, String.format("Cannot initialize: Invalid keyboard type: %s", keyboardType.toString())); @@ -58,6 +67,11 @@ public void setKeyboardLoaded(boolean keyboardLoaded) { public void onPageStarted(WebView view, String url, Bitmap favicon) { } + @Override + public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { + return this.assetLoader.shouldInterceptRequest(request.getUrl()); + } + @Override public void onPageFinished(WebView view, String url) { Log.d("KMEA", String.format("onPageFinished: [%s] %s", keyboardType.toString(), url)); @@ -74,46 +88,44 @@ private void pageLoaded(WebView view, String url) { kmKeyboard.keyboardSet = false; KMManager.currentLexicalModel = null; - if (url.startsWith("file")) { // TODO: is this test necessary? - this.keyboardLoaded = true; + this.keyboardLoaded = true; - SharedPreferences prefs = context.getSharedPreferences(context.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE); - int index = prefs.getInt(KMManager.KMKey_UserKeyboardIndex, 0); - if (index < 0) { - index = 0; + SharedPreferences prefs = context.getSharedPreferences(context.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE); + int index = prefs.getInt(KMManager.KMKey_UserKeyboardIndex, 0); + if (index < 0) { + index = 0; + } + Keyboard keyboardInfo = KMManager.getKeyboardInfo(context, index); + String langId = null; + if (keyboardInfo != null) { + langId = keyboardInfo.getLanguageID(); + kmKeyboard.setKeyboard(keyboardInfo); + } else { + // Revert to default (index 0) or fallback keyboard + keyboardInfo = KMManager.getKeyboardInfo(context, 0); + if (keyboardInfo == null) { + // Don't log to Sentry because some keyboard apps like FV don't install keyboards until the user chooses + keyboardInfo = KMManager.getDefaultKeyboard(context); } - Keyboard keyboardInfo = KMManager.getKeyboardInfo(context, index); - String langId = null; if (keyboardInfo != null) { langId = keyboardInfo.getLanguageID(); kmKeyboard.setKeyboard(keyboardInfo); - } else { - // Revert to default (index 0) or fallback keyboard - keyboardInfo = KMManager.getKeyboardInfo(context, 0); - if (keyboardInfo == null) { - // Don't log to Sentry because some keyboard apps like FV don't install keyboards until the user chooses - keyboardInfo = KMManager.getDefaultKeyboard(context); - } - if (keyboardInfo != null) { - langId = keyboardInfo.getLanguageID(); - kmKeyboard.setKeyboard(keyboardInfo); - } } + } - KMManager.registerAssociatedLexicalModel(langId); + KMManager.registerAssociatedLexicalModel(langId); - kmKeyboard.showHelpBubbleAfterDelay(2000, true); // check if it should be shown at that time! + kmKeyboard.showHelpBubbleAfterDelay(2000, true); // check if it should be shown at that time! - kmKeyboard.callJavascriptAfterLoad(); - kmKeyboard.setSpacebarText(KMManager.getSpacebarText()); + kmKeyboard.callJavascriptAfterLoad(); + kmKeyboard.setSpacebarText(KMManager.getSpacebarText()); - KeyboardEventHandler.notifyListeners(KMTextView.kbEventListeners, keyboardType, EventType.KEYBOARD_LOADED, null); + KeyboardEventHandler.notifyListeners(KMTextView.kbEventListeners, keyboardType, EventType.KEYBOARD_LOADED, null); - // Special handling for in-app TextView context keymanapp/keyman#3809 - if (keyboardType == KeyboardType.KEYBOARD_TYPE_INAPP && - KMTextView.activeView != null && KMTextView.activeView.getClass() == KMTextView.class) { - KMTextView.updateTextContext(); - } + // Special handling for in-app TextView context keymanapp/keyman#3809 + if (keyboardType == KeyboardType.KEYBOARD_TYPE_INAPP && + KMTextView.activeView != null && KMTextView.activeView.getClass() == KMTextView.class) { + KMTextView.updateTextContext(); } } 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 cd9a737d9c8..b300cc27d3b 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 @@ -1,5 +1,5 @@ -/** - * Copyright (C) 2017 SIL International. All rights reserved. +/* + * Keyman is copyright (C) SIL Global. MIT License. */ package com.keyman.engine; @@ -393,6 +393,10 @@ public static String getResourceRoot() { return appContext.getDir("data", Context.MODE_PRIVATE).toString() + File.separator; } + public static String getResourceUrl() { + return WebViewUtils.MAGIC_DEFAULT_DOMAIN +"/data/"; + } + public static String getPackagesDir() { return getResourceRoot() + KMDefault_AssetPackages + File.separator; } @@ -401,6 +405,10 @@ public static String getLexicalModelsDir() { return getResourceRoot() + KMDefault_LexicalModelPackages + File.separator; } + public static String getLexicalModelsUrl() { + return getResourceUrl() + KMDefault_LexicalModelPackages + "/"; + } + public static String getCloudDir() { return getResourceRoot() + KMDefault_UndefinedPackageID + File.separator; } @@ -1646,8 +1654,9 @@ public static boolean registerLexicalModel(HashMap lexicalModelI String modelID = lexicalModelInfo.get(KMKey_LexicalModelID); String languageID = lexicalModelInfo.get(KMKey_LanguageID); boolean modelFileExists = true; - File modelFile = new File(getLexicalModelsDir(), pkgID + File.separator + modelID + ".model.js"); - String path = "file://" + modelFile.getAbsolutePath(); + String modelFilename = pkgID + File.separator + modelID + ".model.js"; + File modelFile = new File(getLexicalModelsDir(), modelFilename); + String url = getLexicalModelsUrl() + modelFilename; // Disable sugestions if lexical-model file doesn't exist if (!modelFile.exists()) { @@ -1662,7 +1671,7 @@ public static boolean registerLexicalModel(HashMap lexicalModelI modelObj.put("id", modelID); languageJSONArray.put(languageID); modelObj.put("languages", languageJSONArray); - modelObj.put("path", path); + modelObj.put("path", url); modelObj.put("CustomHelpLink", lexicalModelInfo.get(KMKey_CustomHelpLink)); } catch (JSONException e) { KMLog.LogException(TAG, "Invalid lexical model to register", e); diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/data/Keyboard.java b/android/KMEA/app/src/main/java/com/keyman/engine/data/Keyboard.java index fb127095b6e..f17fe429159 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/data/Keyboard.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/data/Keyboard.java @@ -1,5 +1,5 @@ -/** - * Copyright (C) 2020 SIL International. All rights reserved. +/* + * Keyman is copyright (C) SIL Global. MIT License. */ package com.keyman.engine.data; @@ -15,6 +15,7 @@ import com.keyman.engine.util.FileUtils; import com.keyman.engine.util.KMLog; import com.keyman.engine.util.KMString; +import com.keyman.engine.util.WebViewUtils; import org.json.JSONArray; import org.json.JSONException; @@ -184,24 +185,23 @@ public JSONObject toJSON() { return o; } - private String getKeyboardRoot(Context context) { - String keyboardRoot = context.getDir("data", Context.MODE_PRIVATE).toString() + - File.separator; - + private String getKeyboardRoot() { + String keyboardRoot = WebViewUtils.MAGIC_DEFAULT_DOMAIN + "/data/"; if (packageID.equals(KMManager.KMDefault_UndefinedPackageID)) { - return keyboardRoot + KMManager.KMDefault_UndefinedPackageID + File.separator; + keyboardRoot += KMManager.KMDefault_UndefinedPackageID + "/"; } else { - return keyboardRoot + KMManager.KMDefault_AssetPackages + File.separator + packageID + File.separator; + keyboardRoot += KMManager.KMDefault_AssetPackages + "/" + packageID + "/"; } + return keyboardRoot; } - public String getKeyboardPath(Context context) { + private String getKeyboardUrl() { String keyboardID = this.getKeyboardID(); String keyboardVersion = this.getVersion(); if (packageID.equals(KMManager.KMDefault_UndefinedPackageID)) { - return getKeyboardRoot(context) + keyboardID + "-" + keyboardVersion + ".js"; + return getKeyboardRoot() + keyboardID + "-" + keyboardVersion + ".js"; } else { - return getKeyboardRoot(context) + keyboardID + ".js"; + return getKeyboardRoot() + keyboardID + ".js"; } } @@ -213,17 +213,17 @@ public String toStub(Context context) { stubObj.put("KI", "Keyboard_" + this.getKeyboardID()); stubObj.put("KLC", this.getLanguageID()); stubObj.put("KL", this.getLanguageName()); - stubObj.put("KF", this.getKeyboardPath(context)); + stubObj.put("KF", this.getKeyboardUrl()); stubObj.put("KP", this.getPackageID()); String displayFont = this.getFont(); if(displayFont != null) { - stubObj.put("KFont", this.buildDisplayFontObject(displayFont, context)); + stubObj.put("KFont", this.buildDisplayFontObject(displayFont)); } String oskFont = this.getOSKFont(); if(oskFont != null) { - stubObj.put("KOskFont", this.buildDisplayFontObject(oskFont, context)); + stubObj.put("KOskFont", this.buildDisplayFontObject(oskFont)); } String displayName = this.getDisplayName(); @@ -245,12 +245,12 @@ public String toStub(Context context) { * @param font String font JSON object as a string * @return JSONObject of modified font information with full paths. If font is invalid, return `null` */ - private JSONObject buildDisplayFontObject(String font, Context context) { + private JSONObject buildDisplayFontObject(String font) { if(font == null || font.equals("")) { return null; } - String keyboardRoot = this.getKeyboardRoot(context); + String keyboardRoot = this.getKeyboardRoot(); try { if (FileUtils.hasFontExtension(font)) { diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/util/WebViewUtils.java b/android/KMEA/app/src/main/java/com/keyman/engine/util/WebViewUtils.java index de8b37f73ed..0a867d0b78a 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/util/WebViewUtils.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/util/WebViewUtils.java @@ -35,6 +35,14 @@ public enum SystemWebViewStatus { private static final String CHROME_INSTALL_PATTERN_FORMATSTR = "^.*Chrome/([\\d.]+).*$"; private static final Pattern installPattern = Pattern.compile(CHROME_INSTALL_PATTERN_FORMATSTR); + /** + * Reserved magic domain for loading files from the local device. At runtime + * the WebViewAssetLoader will replace the protocol and domain with the + * internal storage path. + * See https://developer.android.com/reference/androidx/webkit/WebViewAssetLoader + */ + public static final String MAGIC_DEFAULT_DOMAIN = "https://appassets.androidplatform.net"; + /** * Get the Keyman Engine mode based on the Chrome version. * @param context - The context From 48b03579f31a0f002650acb637175bb6452e11d1 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Mon, 29 Jun 2026 16:15:47 +0200 Subject: [PATCH 02/14] chore(android): address code review comments Co-authored-by: Marc Durdin --- android/KMEA/app/src/main/assets/keyboard.html | 2 +- .../main/java/com/keyman/engine/KMKeyboard.java | 11 ++++------- .../com/keyman/engine/KMKeyboardWebViewClient.java | 11 ++++++----- .../src/main/java/com/keyman/engine/KMManager.java | 6 +----- .../main/java/com/keyman/engine/data/Keyboard.java | 7 ++----- .../java/com/keyman/engine/util/WebViewUtils.java | 14 +++++++++++++- 6 files changed, 27 insertions(+), 24 deletions(-) diff --git a/android/KMEA/app/src/main/assets/keyboard.html b/android/KMEA/app/src/main/assets/keyboard.html index 720703d6e3b..8d9d2d67dcc 100644 --- a/android/KMEA/app/src/main/assets/keyboard.html +++ b/android/KMEA/app/src/main/assets/keyboard.html @@ -1,7 +1,7 @@ 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 eccead67bdc..2f13453466a 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 @@ -86,7 +86,7 @@ final class KMKeyboard extends WebView { private static String txtFont = ""; private static String oskFont = null; - private String keyboardRoot = ""; + private static String keyboardRoot = ""; private final String fontUndefined = "undefined"; private GestureDetector gestureDetector; private static ArrayList kbEventListeners = null; @@ -367,10 +367,7 @@ public void loadKeyboard() { } else { KMManager.SystemKeyboardWebViewClient.setKeyboardLoaded(false); } - - // Use the reserved magic domain for loading the keyboard from the local device. - // See https://developer.android.com/reference/androidx/webkit/WebViewAssetLoader - String htmlPath = WebViewUtils.MAGIC_DEFAULT_DOMAIN + "/data/" + KMManager.KMFilename_KeyboardHtml; + String htmlPath = WebViewUtils.buildAssetUrl(KMManager.KMFilename_KeyboardHtml); loadUrl(htmlPath); setBackgroundColor(0); } @@ -803,9 +800,9 @@ private void sendError(String packageID, String keyboardID, String languageID, b // Set the base path of the keyboard depending on the package ID private void setKeyboardRoot(String packageID) { if (packageID.equals(KMManager.KMDefault_UndefinedPackageID)) { - this.keyboardRoot = WebViewUtils.MAGIC_DEFAULT_DOMAIN + "/data/" + KMManager.KMDefault_UndefinedPackageID + "/"; + this.keyboardRoot = WebViewUtils.buildAssetUrl(KMManager.KMDefault_UndefinedPackageID + "/"); } else { - this.keyboardRoot = WebViewUtils.MAGIC_DEFAULT_DOMAIN + "/data/" + KMManager.KMDefault_AssetPackages + "/" + packageID + "/"; + this.keyboardRoot = WebViewUtils.buildAssetUrl(KMManager.KMDefault_AssetPackages + "/" + packageID + "/"); } } 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 c4a2818e6c6..f043a56681e 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 @@ -23,6 +23,7 @@ import com.keyman.engine.KMManager.SuggestionType; import com.keyman.engine.util.KMLog; import com.keyman.engine.data.Keyboard; +import com.keyman.engine.util.WebViewUtils; import org.json.JSONObject; @@ -42,8 +43,8 @@ public final class KMKeyboardWebViewClient extends WebViewClient { this.keyboardType = keyboardType; this.keyboardLoaded = false; this.assetLoader = new WebViewAssetLoader.Builder() - .addPathHandler("/data/", new InternalStoragePathHandler(context, - context.getDir("data", Context.MODE_PRIVATE))) + .addPathHandler(WebViewUtils.ASSET_DATA_PATH, + new InternalStoragePathHandler(context, context.getDir("data", Context.MODE_PRIVATE))) .build(); if (keyboardType != KeyboardType.KEYBOARD_TYPE_INAPP && keyboardType != KeyboardType.KEYBOARD_TYPE_SYSTEM) { @@ -92,10 +93,10 @@ private void pageLoaded(WebView view, String url) { SharedPreferences prefs = context.getSharedPreferences(context.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE); int index = prefs.getInt(KMManager.KMKey_UserKeyboardIndex, 0); - if (index < 0) { - index = 0; + Keyboard keyboardInfo = null; + if (index >= 0) { + keyboardInfo = KMManager.getKeyboardInfo(context, index); } - Keyboard keyboardInfo = KMManager.getKeyboardInfo(context, index); String langId = null; if (keyboardInfo != null) { langId = keyboardInfo.getLanguageID(); 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 b300cc27d3b..c0b966467de 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 @@ -393,10 +393,6 @@ public static String getResourceRoot() { return appContext.getDir("data", Context.MODE_PRIVATE).toString() + File.separator; } - public static String getResourceUrl() { - return WebViewUtils.MAGIC_DEFAULT_DOMAIN +"/data/"; - } - public static String getPackagesDir() { return getResourceRoot() + KMDefault_AssetPackages + File.separator; } @@ -406,7 +402,7 @@ public static String getLexicalModelsDir() { } public static String getLexicalModelsUrl() { - return getResourceUrl() + KMDefault_LexicalModelPackages + "/"; + return WebViewUtils.buildAssetUrl(KMDefault_LexicalModelPackages + "/"); } public static String getCloudDir() { diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/data/Keyboard.java b/android/KMEA/app/src/main/java/com/keyman/engine/data/Keyboard.java index f17fe429159..5b8b031f81d 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/data/Keyboard.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/data/Keyboard.java @@ -186,13 +186,10 @@ public JSONObject toJSON() { } private String getKeyboardRoot() { - String keyboardRoot = WebViewUtils.MAGIC_DEFAULT_DOMAIN + "/data/"; if (packageID.equals(KMManager.KMDefault_UndefinedPackageID)) { - keyboardRoot += KMManager.KMDefault_UndefinedPackageID + "/"; - } else { - keyboardRoot += KMManager.KMDefault_AssetPackages + "/" + packageID + "/"; + return WebViewUtils.buildAssetUrl(KMManager.KMDefault_UndefinedPackageID + "/"); } - return keyboardRoot; + return WebViewUtils.buildAssetUrl(KMManager.KMDefault_AssetPackages + "/" + packageID + "/"); } private String getKeyboardUrl() { diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/util/WebViewUtils.java b/android/KMEA/app/src/main/java/com/keyman/engine/util/WebViewUtils.java index 0a867d0b78a..34d75e61957 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/util/WebViewUtils.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/util/WebViewUtils.java @@ -41,7 +41,19 @@ public enum SystemWebViewStatus { * internal storage path. * See https://developer.android.com/reference/androidx/webkit/WebViewAssetLoader */ - public static final String MAGIC_DEFAULT_DOMAIN = "https://appassets.androidplatform.net"; + private static final String MAGIC_DEFAULT_DOMAIN = "https://appassets.androidplatform.net"; + + /** + * Path under the asset domain where all assets live + */ + public static final String ASSET_DATA_PATH = "/data/"; + + /** + * Build a full URL to the provided asset + */ + public static String buildAssetUrl(String assetPath) { + return WebViewUtils.MAGIC_DEFAULT_DOMAIN + WebViewUtils.ASSET_DATA_PATH + assetPath; + } /** * Get the Keyman Engine mode based on the Chrome version. From aee5620dbf2593491b261b53ec3080caaa4e9e44 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Mon, 29 Jun 2026 17:56:06 +0200 Subject: [PATCH 03/14] fix(android): add tests for `WebViewUtils.buildAssertUrl` Also fix `assertEquals` in that test file - the parameter ordering is `expected, actual` but we had it the other way round which gives a confusing message if the test fails. --- .../java/com/keyman/engine/util/WebViewUtils.java | 6 ++++-- .../java/com/keyman/engine/util/WebViewUtilsTest.java | 11 +++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/util/WebViewUtils.java b/android/KMEA/app/src/main/java/com/keyman/engine/util/WebViewUtils.java index 34d75e61957..30be0a86a6f 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/util/WebViewUtils.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/util/WebViewUtils.java @@ -41,7 +41,7 @@ public enum SystemWebViewStatus { * internal storage path. * See https://developer.android.com/reference/androidx/webkit/WebViewAssetLoader */ - private static final String MAGIC_DEFAULT_DOMAIN = "https://appassets.androidplatform.net"; + private static final String MAGIC_DEFAULT_DOMAIN = "https://appassets.androidplatform.net"; /** * Path under the asset domain where all assets live @@ -52,7 +52,9 @@ public enum SystemWebViewStatus { * Build a full URL to the provided asset */ public static String buildAssetUrl(String assetPath) { - return WebViewUtils.MAGIC_DEFAULT_DOMAIN + WebViewUtils.ASSET_DATA_PATH + assetPath; + String appendAsset = assetPath == null ? "" : + (assetPath.startsWith("/") ? assetPath.substring(1) : assetPath); + return WebViewUtils.MAGIC_DEFAULT_DOMAIN + WebViewUtils.ASSET_DATA_PATH + appendAsset; } /** diff --git a/android/KMEA/app/src/test/java/com/keyman/engine/util/WebViewUtilsTest.java b/android/KMEA/app/src/test/java/com/keyman/engine/util/WebViewUtilsTest.java index 344b01879a9..c024c939f12 100644 --- a/android/KMEA/app/src/test/java/com/keyman/engine/util/WebViewUtilsTest.java +++ b/android/KMEA/app/src/test/java/com/keyman/engine/util/WebViewUtilsTest.java @@ -57,4 +57,15 @@ public void test_Chrome57_EngineWebViewVersionStatusFull() { Assert.assertEquals(WebViewUtils.getEngineWebViewVersionStatus(context, null, chromeVersion), WebViewUtils.EngineWebViewVersionStatus.FULL); } + + @Test + public void test_buildAssetUrl() { + Assert.assertEquals("https://appassets.androidplatform.net/data/", WebViewUtils.buildAssetUrl("")); + Assert.assertEquals("https://appassets.androidplatform.net/data/", WebViewUtils.buildAssetUrl(null)); + Assert.assertEquals("https://appassets.androidplatform.net/data/foo", WebViewUtils.buildAssetUrl("foo")); + Assert.assertEquals("https://appassets.androidplatform.net/data/foo/", WebViewUtils.buildAssetUrl("foo/")); + Assert.assertEquals("https://appassets.androidplatform.net/data/foo/", WebViewUtils.buildAssetUrl("/foo/")); + Assert.assertEquals("https://appassets.androidplatform.net/data/foo/bar.html", WebViewUtils.buildAssetUrl("foo/bar.html")); + } + } From 8f3ce341f86d7bc08c3f27a74a0752c8d0afef4e Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Mon, 29 Jun 2026 18:22:02 +0200 Subject: [PATCH 04/14] fix(android): fix failing test on Windows On Windows the tests failed because the AppData directory doesn't yet exist under an allowed app internal storage path. This change creates the directory first when running tests. --- .../src/test/java/com/keyman/engine/KMManagerTest.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/android/KMEA/app/src/test/java/com/keyman/engine/KMManagerTest.java b/android/KMEA/app/src/test/java/com/keyman/engine/KMManagerTest.java index 91fc7dd3709..71fa9c5ddbe 100644 --- a/android/KMEA/app/src/test/java/com/keyman/engine/KMManagerTest.java +++ b/android/KMEA/app/src/test/java/com/keyman/engine/KMManagerTest.java @@ -1,5 +1,9 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ package com.keyman.engine; +import android.content.Context; import android.util.Log; import androidx.test.core.app.ApplicationProvider; @@ -32,7 +36,11 @@ public class KMManagerTest { // For some keyboard list tests, load an existing keyboard list. // Can't use @Before because context is null before running tests. public void loadOldKeyboardsList() { - KMManager.initialize(ApplicationProvider.getApplicationContext(), KMManager.KeyboardType.KEYBOARD_TYPE_INAPP); + Context context = ApplicationProvider.getApplicationContext(); + // Create appData directory accessed in KMKeyboardWebViewClient + File dataDir = context.getDir("data", Context.MODE_PRIVATE); + dataDir.mkdirs(); + KMManager.initialize(context, KMManager.KeyboardType.KEYBOARD_TYPE_INAPP); File keyboards_dat = new File(TEST_RESOURCE_ROOT, OLD_KEYBOARDS_LIST); if (keyboards_dat == null || !keyboards_dat.exists()) { From 3e087ac186016bdc136efdc5791c33fee444a4c8 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Tue, 30 Jun 2026 11:31:51 +0200 Subject: [PATCH 05/14] fix(android): add missing dependency for FirstVoices and Samples Fixes: FV-ANDROID-20E Build-bot: release:android --- android/Samples/KMSample1/app/build.gradle | 1 + android/Samples/KMSample2/app/build.gradle | 1 + oem/firstvoices/android/app/build.gradle | 1 + 3 files changed, 3 insertions(+) diff --git a/android/Samples/KMSample1/app/build.gradle b/android/Samples/KMSample1/app/build.gradle index 65c0ecb8c9f..da949d36d1c 100644 --- a/android/Samples/KMSample1/app/build.gradle +++ b/android/Samples/KMSample1/app/build.gradle @@ -41,4 +41,5 @@ dependencies { implementation 'com.google.android.material:material:1.12.0' api(name: 'keyman-engine', ext: 'aar') implementation 'androidx.preference:preference:1.2.1' + implementation 'androidx.webkit:webkit:1.14.0' } diff --git a/android/Samples/KMSample2/app/build.gradle b/android/Samples/KMSample2/app/build.gradle index a7e47393207..45d5dc1c7f0 100644 --- a/android/Samples/KMSample2/app/build.gradle +++ b/android/Samples/KMSample2/app/build.gradle @@ -40,4 +40,5 @@ dependencies { implementation 'com.google.android.material:material:1.12.0' api (name:'keyman-engine', ext:'aar') implementation 'androidx.preference:preference:1.2.1' + implementation 'androidx.webkit:webkit:1.14.0' } diff --git a/oem/firstvoices/android/app/build.gradle b/oem/firstvoices/android/app/build.gradle index d341db7a80a..ff5f15628bd 100644 --- a/oem/firstvoices/android/app/build.gradle +++ b/oem/firstvoices/android/app/build.gradle @@ -134,6 +134,7 @@ dependencies { api(name: 'keyman-engine', ext: 'aar') implementation 'io.sentry:sentry-android:7.22.6' implementation 'androidx.preference:preference:1.2.1' + implementation 'androidx.webkit:webkit:1.14.0' } apply plugin: 'com.android.application' From 052b40a90f1bdce709c9a4ed3d4f18d20fa54d96 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Tue, 30 Jun 2026 12:56:50 +0200 Subject: [PATCH 06/14] fix(android): add missing dependency for KeyboardHarness app --- android/Tests/KeyboardHarness/app/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/android/Tests/KeyboardHarness/app/build.gradle b/android/Tests/KeyboardHarness/app/build.gradle index c53d3e97391..1b015cf1c4d 100644 --- a/android/Tests/KeyboardHarness/app/build.gradle +++ b/android/Tests/KeyboardHarness/app/build.gradle @@ -51,4 +51,5 @@ dependencies { implementation 'com.google.android.material:material:1.12.0' api (name:'keyman-engine', ext:'aar') implementation 'androidx.preference:preference:1.2.1' + implementation 'androidx.webkit:webkit:1.14.0' } From ed21a0f780e33fe06677b153c9e8e5fa7e51d566 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Wed, 1 Jul 2026 11:27:19 +0200 Subject: [PATCH 07/14] fix(android): fix problem with fonts The default fonts (DejaVueSans.ttf and keymanweb-osk.ttf) are located in the root directory, not the package directory. This fixes the KeyboardHarness test app. Also some improvements with variable and function names to better match what they do. --- .../java/com/keyman/engine/KMKeyboard.java | 71 ++++++++++++------- .../java/com/keyman/engine/KMManager.java | 13 ++++ .../java/com/keyman/engine/data/Keyboard.java | 33 +++++---- 3 files changed, 76 insertions(+), 41 deletions(-) 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 2f13453466a..5659a3ebce4 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 @@ -86,7 +86,8 @@ final class KMKeyboard extends WebView { private static String txtFont = ""; private static String oskFont = null; - private static String keyboardRoot = ""; + private static String dataRoot = ""; + private static String packageRoot = ""; private final String fontUndefined = "undefined"; private GestureDetector gestureDetector; private static ArrayList kbEventListeners = null; @@ -635,7 +636,7 @@ public boolean prepareKeyboardSwitch(String packageID, String keyboardID, String } String kbKey = KMString.format("%s_%s", languageID, keyboardID); - setKeyboardRoot(packageID); + setPackageRoot(packageID); // Escape single-quoted names for javascript call keyboardName = keyboardName.replaceAll("\'", "\\\\'"); // Double-escaped-backslash b/c regex. @@ -691,13 +692,13 @@ public boolean setKeyboard(String packageID, String keyboardID, String languageI KMManager.getLatestKeyboardFileVersion(getContext(), packageID, keyboardID) : null; } - setKeyboardRoot(packageID); + setPackageRoot(packageID); if(kOskFont == null || kOskFont.isEmpty()) kOskFont = kFont; - JSONObject jDisplayFont = makeFontPaths(kFont); - JSONObject jOskFont = makeFontPaths(kOskFont); + JSONObject jDisplayFont = makeFontObject(kFont); + JSONObject jOskFont = makeFontObject(kOskFont); txtFont = getFontFilename(jDisplayFont); oskFont = getFontFilename(jOskFont); @@ -798,20 +799,25 @@ private void sendError(String packageID, String keyboardID, String languageID, b } // Set the base path of the keyboard depending on the package ID - private void setKeyboardRoot(String packageID) { + private void setPackageRoot(String packageID) { + this.dataRoot = WebViewUtils.buildAssetUrl(""); if (packageID.equals(KMManager.KMDefault_UndefinedPackageID)) { - this.keyboardRoot = WebViewUtils.buildAssetUrl(KMManager.KMDefault_UndefinedPackageID + "/"); + this.packageRoot = this.dataRoot + KMManager.KMDefault_UndefinedPackageID + "/"; } else { - this.keyboardRoot = WebViewUtils.buildAssetUrl(KMManager.KMDefault_AssetPackages + "/" + packageID + "/"); + this.packageRoot = this.dataRoot + KMManager.KMDefault_AssetPackages + "/" + packageID + "/"; } } - private String getKeyboardRoot() { - return this.keyboardRoot; + private String getDataRoot() { + return this.dataRoot; + } + + private String getPackageRoot() { + return this.packageRoot; } private String makeKeyboardUrl(String packageID, String keyboardID, String keyboardVersion) { - String keyboardUrl = getKeyboardRoot(); + String keyboardUrl = getPackageRoot(); if (packageID.equals(KMManager.KMDefault_UndefinedPackageID)) { keyboardUrl += keyboardID + "-" + keyboardVersion + ".js"; } else { @@ -1029,13 +1035,26 @@ public void onDismiss() { } /** - * Take a font JSON object and adjust to pass to JS - * 1. Replace "source" keys for "files" keys - * 2. Create full font paths for .ttf or .svg - * @param font String font JSON object as a string - * @return JSONObject of modified font information with full paths. If font is invalid, return `null` + * Create a JSON object consisting of the font family and the URLs of the + * font files on the local device. + * + * The `font` parameter can either be the filename of the font (with an + * extension recognized as font), or a Font object or JSON string. + * In the former case a new JSON object is created with the font family + * derived from the filename, and the font filename prefixed with path + * to the fonts. + * In the latter case the legacy `sources` key is renamed to `files`. + * If `files` is a single string it will be prefixed with the path to the + * fonts. If `files` is an array, the array is iterated until finding + * the first file with a font extension which is then prefixed with the + * path to the fonts. + * + * @param font A string containing either the font filename or a font JSON + * object as a string + * @return JSONObject of modified font information with full paths. If font + * is invalid, return `null`. */ - private JSONObject makeFontPaths(String font) { + private JSONObject makeFontObject(String font) { if(font == null || font.equals("")) { return null; @@ -1046,14 +1065,13 @@ private JSONObject makeFontPaths(String font) { JSONObject jfont = new JSONObject(); jfont.put(KMManager.KMKey_FontFamily, font.substring(0, font.length()-4)); JSONArray jfiles = new JSONArray(); - jfiles.put(getKeyboardRoot() + font); + String fontRoot = KMManager.isDefaultFont(font) ? getDataRoot() : getPackageRoot(); + jfiles.put(fontRoot + font); jfont.put(KMManager.KMKey_FontFiles, jfiles); return jfont; } JSONObject fontObj = new JSONObject(font); - JSONArray sourceArray; - String fontFile; // Replace "sources" key with "files" if (fontObj.has(KMManager.KMKey_FontSource)) { @@ -1063,16 +1081,18 @@ private JSONObject makeFontPaths(String font) { Object obj = fontObj.get(KMManager.KMKey_FontFiles); if (obj instanceof String) { - fontFile = fontObj.getString(KMManager.KMKey_FontFiles); - fontObj.put(KMManager.KMKey_FontFiles, getKeyboardRoot() + obj); + String fontFile = fontObj.getString(KMManager.KMKey_FontFiles); + String fontRoot = KMManager.isDefaultFont(fontFile) ? getDataRoot() : getPackageRoot(); + fontObj.put(KMManager.KMKey_FontFiles, fontRoot + obj); return fontObj; } else if (obj instanceof JSONArray) { - sourceArray = fontObj.optJSONArray(KMManager.KMKey_FontFiles); + JSONArray sourceArray = fontObj.optJSONArray(KMManager.KMKey_FontFiles); if (sourceArray != null) { for (int i = 0; i < sourceArray.length(); i++) { - fontFile = sourceArray.getString(i); + String fontFile = sourceArray.getString(i); if (FileUtils.hasFontExtension(fontFile)) { - fontObj.put(KMManager.KMKey_FontFiles, getKeyboardRoot() + fontFile); + String fontRoot = KMManager.isDefaultFont(fontFile) ? getDataRoot() : getPackageRoot(); + fontObj.put(KMManager.KMKey_FontFiles, fontRoot + fontFile); fontObj.remove(KMManager.KMKey_FontSource); return fontObj; } @@ -1081,7 +1101,6 @@ private JSONObject makeFontPaths(String font) { } } catch (JSONException e) { KMLog.LogException(TAG, "Failed to make font for '"+font+"'", e); - return null; } return null; 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 c0b966467de..dbc930b0af9 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 @@ -495,6 +495,19 @@ public static boolean isReservedNamespace(String packageID) { return false; } + /** + * Check if the given font is the default font or the default OSK font. + * @param fontName String - The font name and extension + * @return boolean true if `fontName` is DejaVueSans.ttf or keymanweb-osk.ttf + * or null or empty string, false otherwise + */ + public static boolean isDefaultFont(String fontName) { + if (fontName == null || fontName.isEmpty()) { + return true; + } + return fontName.equals(KMDefault_KeyboardFont) || fontName.equals(KMFilename_Osk_Ttf_Font); + } + public static void initialize(final Context context, KeyboardType keyboardType) { appContext = context.getApplicationContext(); diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/data/Keyboard.java b/android/KMEA/app/src/main/java/com/keyman/engine/data/Keyboard.java index 5b8b031f81d..896fb23e965 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/data/Keyboard.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/data/Keyboard.java @@ -185,20 +185,24 @@ public JSONObject toJSON() { return o; } - private String getKeyboardRoot() { + private String getDataRoot() { + return WebViewUtils.buildAssetUrl(""); + } + + private String getPackageRoot() { if (packageID.equals(KMManager.KMDefault_UndefinedPackageID)) { - return WebViewUtils.buildAssetUrl(KMManager.KMDefault_UndefinedPackageID + "/"); + return getDataRoot() + KMManager.KMDefault_UndefinedPackageID + "/"; } - return WebViewUtils.buildAssetUrl(KMManager.KMDefault_AssetPackages + "/" + packageID + "/"); + return getDataRoot() + KMManager.KMDefault_AssetPackages + "/" + packageID + "/"; } private String getKeyboardUrl() { String keyboardID = this.getKeyboardID(); String keyboardVersion = this.getVersion(); if (packageID.equals(KMManager.KMDefault_UndefinedPackageID)) { - return getKeyboardRoot() + keyboardID + "-" + keyboardVersion + ".js"; + return getPackageRoot() + keyboardID + "-" + keyboardVersion + ".js"; } else { - return getKeyboardRoot() + keyboardID + ".js"; + return getPackageRoot() + keyboardID + ".js"; } } @@ -236,34 +240,33 @@ public String toStub(Context context) { } /** - * Take a font JSON object and adjust to pass to JS - * 1. Replace "source" keys for "files" keys - * 2. Create full font paths for .ttf or .svg - * @param font String font JSON object as a string - * @return JSONObject of modified font information with full paths. If font is invalid, return `null` + * Create a JSON object consisting of the font family and the font + * file URL. + * + * @param font A string containing the font filename + * @return JSONObject of modified font information with full URL. If font + * is invalid, return `null`. */ private JSONObject buildDisplayFontObject(String font) { if(font == null || font.equals("")) { return null; } - String keyboardRoot = this.getKeyboardRoot(); + String fontRoot = KMManager.isDefaultFont(font) ? this.getDataRoot() : this.getPackageRoot(); try { if (FileUtils.hasFontExtension(font)) { JSONObject jfont = new JSONObject(); jfont.put(KMManager.KMKey_FontFamily, font.substring(0, font.length() - 4)); JSONArray jfiles = new JSONArray(); - jfiles.put(keyboardRoot + font); + jfiles.put(fontRoot + font); jfont.put(KMManager.KMKey_FontFiles, jfiles); return jfont; - } else { - return null; } } catch (JSONException e) { KMLog.LogException(TAG, "Failed to make font for '"+font+"'", e); - return null; } + return null; } /** From 9a04726d3aa86162362d259ff874b95848d7e911 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Wed, 1 Jul 2026 11:52:02 +0200 Subject: [PATCH 08/14] fix(android): fix typo --- android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 dbc930b0af9..a200e94b5de 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 @@ -498,7 +498,7 @@ public static boolean isReservedNamespace(String packageID) { /** * Check if the given font is the default font or the default OSK font. * @param fontName String - The font name and extension - * @return boolean true if `fontName` is DejaVueSans.ttf or keymanweb-osk.ttf + * @return boolean true if `fontName` is DejaVuSans.ttf or keymanweb-osk.ttf * or null or empty string, false otherwise */ public static boolean isDefaultFont(String fontName) { From bdef84ebb430704f9279f2963feb5c86b6df39b4 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Wed, 1 Jul 2026 12:27:38 +0200 Subject: [PATCH 09/14] fix(android): suppress error for fallback keyboard Cherry-pick-of: #16146 We use SIL EuroLatin as fallback keyboard. If that is not installed, previously we output an error which showed up as a toast for the user. This change now outputs a warning instead of the error for the default keyboard. Also changed the KeyboardHarness test app to set a different keyoard as default. --- .../app/src/main/java/com/keyman/engine/KMManager.java | 7 ++++++- .../keyman/android/tests/keyboardHarness/MainActivity.java | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) 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 a200e94b5de..799f18d658b 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 @@ -2239,7 +2239,12 @@ public boolean accept(File pathname) { File kmpJSONFile = new File(path); if (!kmpJSONFile.exists()) { if (!KMManager.isTestMode()) { - KMLog.LogError(TAG, path + " not found. Returning version 1.0"); + if (packageID.equals(KMDefault_PackageID)) { + // Suppress error log for the default fallback keyboard if it's not installed + Log.d(TAG, path + " not found. Returning version 1.0"); + } else { + KMLog.LogError(TAG, path + " not found. Returning version 1.0"); + } } return "1.0"; } diff --git a/android/Tests/KeyboardHarness/app/src/main/java/com/keyman/android/tests/keyboardHarness/MainActivity.java b/android/Tests/KeyboardHarness/app/src/main/java/com/keyman/android/tests/keyboardHarness/MainActivity.java index 7b73341786a..6b5995bf06e 100644 --- a/android/Tests/KeyboardHarness/app/src/main/java/com/keyman/android/tests/keyboardHarness/MainActivity.java +++ b/android/Tests/KeyboardHarness/app/src/main/java/com/keyman/android/tests/keyboardHarness/MainActivity.java @@ -58,6 +58,7 @@ protected void onCreate(Bundle savedInstanceState) { KMManager.KMDefault_KeyboardFont, KMManager.KMDefault_KeyboardFont); KMManager.addKeyboard(this, chiralityKBInfo); + Keyboard.setDefaultKeyboard(chiralityKBInfo); // Longpress test keyboard Keyboard longpressKBbInfo = new Keyboard( From 65b02ae75f27e9b62f7008c77ec9cde0174dceb2 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Thu, 2 Jul 2026 09:16:17 +0200 Subject: [PATCH 10/14] fix(android): skip test on Windows that triggers androidx WebKit bug Fixes: #16183 Test-bot: skip --- .../test/java/com/keyman/engine/KMManagerTest.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/android/KMEA/app/src/test/java/com/keyman/engine/KMManagerTest.java b/android/KMEA/app/src/test/java/com/keyman/engine/KMManagerTest.java index 71fa9c5ddbe..afc206c9da6 100644 --- a/android/KMEA/app/src/test/java/com/keyman/engine/KMManagerTest.java +++ b/android/KMEA/app/src/test/java/com/keyman/engine/KMManagerTest.java @@ -119,6 +119,11 @@ public void test_getTier() { */ @Test public void create_newKeyboardsList() { + if(File.separatorChar != '/') { + Log.w(TAG, "create_newKeyboardsList skipped due to #16183"); + return; + } + loadOldKeyboardsList(); dat_list = new ArrayList>(); @@ -190,6 +195,12 @@ public void create_newKeyboardsList() { @Test public void test_updateOldKeyboardsList() { + // #16183 + if(File.separatorChar != '/') { + Log.w(TAG, "test_updateOldKeyboardsList skipped due to #16183"); + return; + } + loadOldKeyboardsList(); Assert.assertNotNull(dat_list); From a89dd1bed09a26c76f53edb87348f6f2d1da74df Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Thu, 2 Jul 2026 18:22:40 +0200 Subject: [PATCH 11/14] fix(android): fix setting font PR #16146 introduced a problem with selecting a different font as display font so that we always ended up with not setting the font. This was caused by the font filenames now being a URL (which is necessary because they get processed by the web engine). However, the Android code checks for the existence of the font in order to create the typeface, which only works for local paths. This PR modifies and simplifies `KMKeyboard.getFontFilename` to return the full path, renames `KMKeyboard.txtFont` and `KMKeyboard.oskFont` to make it clearer that they contain a path and not a URL. Also initialize `KMKeyboard.oskFontPath` with empty string instead of `null`. This makes it consistent with` txtFontPath` and with the documented behavior of `KMManager.getKeyboardOskFontFilename()` (which returns `KMKeyboard.oskFontPath`). Follows: #16146 Fixes: #16187 Build-bot: release:android --- .../kmapro/WebBrowserActivity.java | 20 ++- .../java/com/keyman/engine/KMKeyboard.java | 119 ++++++++---------- .../java/com/keyman/engine/KMManager.java | 10 ++ .../docs/engine/KMManager/getFontTypeface.md | 9 +- .../KMManager/getKeyboardOskFontFilename.md | 12 +- .../KMManager/getKeyboardTextFontFilename.md | 12 +- 6 files changed, 96 insertions(+), 86 deletions(-) diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/WebBrowserActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/WebBrowserActivity.java index 3308caff9ac..e7bda5b94e9 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/WebBrowserActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/WebBrowserActivity.java @@ -1,9 +1,10 @@ -/** - * Copyright (C) 2017 SIL International. All rights reserved. +/* + * Keyman is copyright (C) SIL Global. MIT License. */ package com.tavultesoft.kmapro; +import java.io.File; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URL; @@ -386,7 +387,7 @@ protected void onResume() { super.onResume(); if (webView != null) { if (didFinishLoading) { - String fontFilename = KMManager.getKeyboardTextFontFilename(); + String fontFilename = getKeyboardTextFontFilename(); if (!loadedFont.equals(fontFilename)) { webView.reload(); } @@ -432,8 +433,19 @@ public void onBackPressed() { } } + /** + * Returns the filename without path of the display font of the current keyboard. + */ + private String getKeyboardTextFontFilename() { + String fontPath = KMManager.getKeyboardTextFontFilename(); + if (fontPath == null || fontPath.isEmpty()) { + return ""; + } + return new File(fontPath).getName(); + } + private void loadFont() { - String font = KMManager.getKeyboardTextFontFilename(); + String font = getKeyboardTextFontFilename(); if (!font.isEmpty()) { loadedFont = font; String fontUrl = String.format("%s%s", fontBaseUri, font); 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 5659a3ebce4..a07d4a83b04 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 @@ -84,10 +84,8 @@ final class KMKeyboard extends WebView { */ protected static KMManager.BannerType currentBanner = KMManager.BannerType.HTML; - private static String txtFont = ""; - private static String oskFont = null; - private static String dataRoot = ""; - private static String packageRoot = ""; + private static String txtFontPath = ""; + private static String oskFontPath = ""; private final String fontUndefined = "undefined"; private GestureDetector gestureDetector; private static ArrayList kbEventListeners = null; @@ -539,7 +537,7 @@ protected void toggleSuggestionBanner(HashMap associatedLexicalM * @return String */ public static String textFontFilename() { - return txtFont; + return txtFontPath; } /** @@ -547,10 +545,10 @@ public static String textFontFilename() { * @return String */ public static String oskFontFilename() { - return oskFont; + return oskFontPath; } - // REVIEW: this method seems to be unused + // REVIEW: this method seems to be unused and undocumented. Can we remove it? /** * Return the full path to the special OSK font, * which is with all the keyboard assets at the root app_data folder @@ -636,8 +634,6 @@ public boolean prepareKeyboardSwitch(String packageID, String keyboardID, String } String kbKey = KMString.format("%s_%s", languageID, keyboardID); - setPackageRoot(packageID); - // Escape single-quoted names for javascript call keyboardName = keyboardName.replaceAll("\'", "\\\\'"); // Double-escaped-backslash b/c regex. @@ -692,16 +688,14 @@ public boolean setKeyboard(String packageID, String keyboardID, String languageI KMManager.getLatestKeyboardFileVersion(getContext(), packageID, keyboardID) : null; } - setPackageRoot(packageID); - if(kOskFont == null || kOskFont.isEmpty()) kOskFont = kFont; - JSONObject jDisplayFont = makeFontObject(kFont); - JSONObject jOskFont = makeFontObject(kOskFont); + JSONObject jDisplayFont = makeFontObject(kFont, packageID); + JSONObject jOskFont = makeFontObject(kOskFont, packageID); - txtFont = getFontFilename(jDisplayFont); - oskFont = getFontFilename(jOskFont); + txtFontPath = getFontFilename(kFont, packageID); + oskFontPath = getFontFilename(kOskFont, packageID); String kbKey = KMString.format("%s_%s", languageID, keyboardID); @@ -798,26 +792,30 @@ private void sendError(String packageID, String keyboardID, String languageID, b } } - // Set the base path of the keyboard depending on the package ID - private void setPackageRoot(String packageID) { - this.dataRoot = WebViewUtils.buildAssetUrl(""); - if (packageID.equals(KMManager.KMDefault_UndefinedPackageID)) { - this.packageRoot = this.dataRoot + KMManager.KMDefault_UndefinedPackageID + "/"; - } else { - this.packageRoot = this.dataRoot + KMManager.KMDefault_AssetPackages + "/" + packageID + "/"; - } + private String getDataRootUrl() { + return WebViewUtils.buildAssetUrl(""); } - private String getDataRoot() { - return this.dataRoot; + private String getDataRootPath() { + return context.getDir("data", Context.MODE_PRIVATE).toString() + File.separator; } - private String getPackageRoot() { - return this.packageRoot; + private String getPackageRootUrl(String packageID) { + if (packageID.equals(KMManager.KMDefault_UndefinedPackageID)) { + return getDataRootUrl() + KMManager.KMDefault_UndefinedPackageID + "/"; + } + return getDataRootUrl() + KMManager.KMDefault_AssetPackages + "/" + packageID + "/"; + } + + private String getPackageRootPath(String packageID) { + if (packageID.equals(KMManager.KMDefault_UndefinedPackageID)) { + return getDataRootPath() + KMManager.KMDefault_UndefinedPackageID + File.separator; + } + return getDataRootPath() + KMManager.KMDefault_AssetPackages + File.separator + packageID + File.separator; } private String makeKeyboardUrl(String packageID, String keyboardID, String keyboardVersion) { - String keyboardUrl = getPackageRoot(); + String keyboardUrl = getPackageRootUrl(packageID); if (packageID.equals(KMManager.KMDefault_UndefinedPackageID)) { keyboardUrl += keyboardID + "-" + keyboardVersion + ".js"; } else { @@ -905,42 +903,25 @@ private void saveCurrentKeyboardIndex() { } /** - * getFontFilename - * Parse a Font JSON object and return the font filename (ending in .ttf or .otf) - * @param fontObj JSONObject - Font JSON object - * @return String - Filename for the font. If font is invalid, return "" + * Return the full path to the font file. If the font is invalid, return empty string. + * @param font String - Font filename + * @param packageID String - Package ID + * @return String - Full path to the font file. If font is invalid, return "". */ - private String getFontFilename(JSONObject fontObj) { - String font = ""; - if (fontObj == null) { - return font; + private String getFontFilename(String font, String packageID) { + if(font == null || font.equals("")) { + return ""; } - try { - JSONArray sourceArray = fontObj.optJSONArray(KMManager.KMKey_FontFiles); - if (sourceArray != null) { - String fontFile; - int length = sourceArray.length(); - for (int i = 0; i < length; i++) { - fontFile = sourceArray.getString(i); - if (FileUtils.hasFontExtension(fontFile)) { - font = fontFile; - break; - } - } - } else { - String fontFile = fontObj.optString(KMManager.KMKey_FontFiles); - if (fontFile != null) { - if (FileUtils.hasFontExtension(fontFile)) { - font = fontFile; - } - } - } - } catch (JSONException e) { - KMLog.LogException(TAG, "", e); - font = ""; + + if (FileUtils.hasFontExtension(font)) { + String fontRoot = KMManager.isDefaultFont(font) ? getDataRootPath() : getPackageRootPath(packageID); + return fontRoot + font; } - return font; + // REVIEW: Do we have to do anything if font is a JSONObject? + // See makeFontObj. + + return ""; } @SuppressLint("InflateParams") @@ -1049,12 +1030,14 @@ public void onDismiss() { * the first file with a font extension which is then prefixed with the * path to the fonts. * - * @param font A string containing either the font filename or a font JSON - * object as a string + * @param font A string containing either the font filename or a font + * JSON object as a string + * @param packageID The package ID of the keyboard + * * @return JSONObject of modified font information with full paths. If font * is invalid, return `null`. */ - private JSONObject makeFontObject(String font) { + private JSONObject makeFontObject(String font, String packageID) { if(font == null || font.equals("")) { return null; @@ -1065,12 +1048,16 @@ private JSONObject makeFontObject(String font) { JSONObject jfont = new JSONObject(); jfont.put(KMManager.KMKey_FontFamily, font.substring(0, font.length()-4)); JSONArray jfiles = new JSONArray(); - String fontRoot = KMManager.isDefaultFont(font) ? getDataRoot() : getPackageRoot(); + String fontRoot = KMManager.isDefaultFont(font) ? getDataRootUrl() : getPackageRootUrl(packageID); jfiles.put(fontRoot + font); jfont.put(KMManager.KMKey_FontFiles, jfiles); return jfont; } + // REVIEW: Why do we need the complicated code below? Can this still + // happen, or can we remove it? (see also getFontFilename) + KMLog.LogInfo(TAG, "Got font without font extension: " + font); + JSONObject fontObj = new JSONObject(font); // Replace "sources" key with "files" @@ -1082,7 +1069,7 @@ private JSONObject makeFontObject(String font) { Object obj = fontObj.get(KMManager.KMKey_FontFiles); if (obj instanceof String) { String fontFile = fontObj.getString(KMManager.KMKey_FontFiles); - String fontRoot = KMManager.isDefaultFont(fontFile) ? getDataRoot() : getPackageRoot(); + String fontRoot = KMManager.isDefaultFont(fontFile) ? getDataRootUrl() : getPackageRootUrl(packageID); fontObj.put(KMManager.KMKey_FontFiles, fontRoot + obj); return fontObj; } else if (obj instanceof JSONArray) { @@ -1091,7 +1078,7 @@ private JSONObject makeFontObject(String font) { for (int i = 0; i < sourceArray.length(); i++) { String fontFile = sourceArray.getString(i); if (FileUtils.hasFontExtension(fontFile)) { - String fontRoot = KMManager.isDefaultFont(fontFile) ? getDataRoot() : getPackageRoot(); + String fontRoot = KMManager.isDefaultFont(fontFile) ? getDataRootUrl() : getPackageRootUrl(packageID); fontObj.put(KMManager.KMKey_FontFiles, fontRoot + fontFile); fontObj.remove(KMManager.KMKey_FontSource); return fontObj; 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 799f18d658b..9314c26e28e 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 @@ -1625,6 +1625,16 @@ public static void setMaySendCrashReport(boolean override) { */ public static Typeface getFontTypeface(Context context, String fontFilename) { try { + if (fontFilename == null || fontFilename.isEmpty()) { + return null; + } + if (fontFilename.startsWith("http://") || fontFilename.startsWith("https://") + || fontFilename.startsWith("file://")) { + // Font file is not local, so cannot load Typeface + KMLog.LogError(TAG, "Font file is not local: " + fontFilename); + return null; + } + if ((fontFilename != null) && FileUtils.hasFontExtension(fontFilename)) { // Ignore .woff files if Android 7.0 / 7.1 (Issue #4896) if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) && diff --git a/android/docs/engine/KMManager/getFontTypeface.md b/android/docs/engine/KMManager/getFontTypeface.md index 4149d92b360..5de7def6bd3 100644 --- a/android/docs/engine/KMManager/getFontTypeface.md +++ b/android/docs/engine/KMManager/getFontTypeface.md @@ -19,7 +19,7 @@ KMManager.getFontTypeface(Context context, String fontFilename) : The context. `fontFilename` -: The filename of the font. +: The filename and full path of the font. ### Returns @@ -29,7 +29,7 @@ if it exists, `null` otherwise. ## Description Use this method to create a new typeface from the font file with -specified filename if it exists in `assets/fonts/` folder. +specified filename if it exists. ## Examples @@ -37,9 +37,10 @@ specified filename if it exists in `assets/fonts/` folder. The following script illustrate the use of `getFontTypeface()`: -``` javascript +``` java KMTextView textView = (KMTextView) findViewById(R.id.kmTextView); - Typeface fontTypeface = KMManager.getFontTypeface(this, "aava1.ttf"); + Typeface fontTypeface = KMManager.getFontTypeface(this, + getDir("data", Context.MODE_PRIVATE).toString() + File.separator + "aava1.ttf"); textView.setTypeface(fontTypeface); ``` diff --git a/android/docs/engine/KMManager/getKeyboardOskFontFilename.md b/android/docs/engine/KMManager/getKeyboardOskFontFilename.md index 8465ea4e8c4..8644130a3cb 100644 --- a/android/docs/engine/KMManager/getKeyboardOskFontFilename.md +++ b/android/docs/engine/KMManager/getKeyboardOskFontFilename.md @@ -5,18 +5,18 @@ title: KMManager.getKeyboardOskFontFilename() ## Summary The **`getKeyboardOskFontFilename()`** method returns the selected -keyboard's OSK font filename. +keyboard's OSK font filename and full path. ## Syntax -``` javascript +``` java KMManager.getKeyboardOskFontFilename() ``` ### Returns -Returns the selected keyboard's OSK font filename as `String` if it has -any, empty string otherwise. +Returns the selected keyboard's OSK font filename and full path as +`String` if it has any, empty string otherwise. ## Description @@ -29,8 +29,8 @@ Use this method to get the OSK font filename of the selected keyboard. The following script illustrate the use of `getKeyboardOskFontFilename()`: -``` javascript - String oskFontFilename = KMManager.getKeyboardOskFontFilename(); +``` java +String oskFontFilename = KMManager.getKeyboardOskFontFilename(); ``` ## See also diff --git a/android/docs/engine/KMManager/getKeyboardTextFontFilename.md b/android/docs/engine/KMManager/getKeyboardTextFontFilename.md index 5c381cc23a0..f1e26e51a37 100644 --- a/android/docs/engine/KMManager/getKeyboardTextFontFilename.md +++ b/android/docs/engine/KMManager/getKeyboardTextFontFilename.md @@ -5,18 +5,18 @@ title: KMManager.getKeyboardTextFontFilename() ## Summary The **`getKeyboardTextFontFilename()`** method returns the selected -keyboard's text font filename. +keyboard's text font filename and full path. ## Syntax -``` javascript +``` java KMManager.getKeyboardTextFontFilename() ``` ### Returns -Returns the selected keyboard's text font filename as `String` if it has -any, empty string otherwise. +Returns the selected keyboard's text font filename and full path as +`String` if it has any, empty string otherwise. ## Description @@ -29,8 +29,8 @@ Use this method to get the text font filename of the selected keyboard. The following script illustrate the use of `getKeyboardTextFontFilename()`: -``` javascript - String textFontFilename = KMManager.getKeyboardTextFontFilename(); +``` java +String textFontFilename = KMManager.getKeyboardTextFontFilename(); ``` ## See also From 30063da5ce1660bdd749268baf8d1a663748fa88 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Fri, 3 Jul 2026 17:23:19 +0200 Subject: [PATCH 12/14] Apply suggestions from code review Co-authored-by: Marc Durdin Co-authored-by: Eberhard Beilharz --- android/docs/engine/KMManager/getFontTypeface.md | 4 ++-- .../docs/engine/KMManager/getKeyboardOskFontFilename.md | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/android/docs/engine/KMManager/getFontTypeface.md b/android/docs/engine/KMManager/getFontTypeface.md index 5de7def6bd3..9dd86928e71 100644 --- a/android/docs/engine/KMManager/getFontTypeface.md +++ b/android/docs/engine/KMManager/getFontTypeface.md @@ -39,8 +39,8 @@ The following script illustrate the use of `getFontTypeface()`: ``` java KMTextView textView = (KMTextView) findViewById(R.id.kmTextView); - Typeface fontTypeface = KMManager.getFontTypeface(this, - getDir("data", Context.MODE_PRIVATE).toString() + File.separator + "aava1.ttf"); + String textFontFilename = KMManager.getKeyboardTextFontFilename(); + Typeface fontTypeface = KMManager.getFontTypeface(this, textFontFilename); textView.setTypeface(fontTypeface); ``` diff --git a/android/docs/engine/KMManager/getKeyboardOskFontFilename.md b/android/docs/engine/KMManager/getKeyboardOskFontFilename.md index 8644130a3cb..7406effb9d5 100644 --- a/android/docs/engine/KMManager/getKeyboardOskFontFilename.md +++ b/android/docs/engine/KMManager/getKeyboardOskFontFilename.md @@ -16,7 +16,14 @@ KMManager.getKeyboardOskFontFilename() ### Returns Returns the selected keyboard's OSK font filename and full path as -`String` if it has any, empty string otherwise. +`String` if it has any, empty string otherwise. Note that the +on-screen keyboard will fallback to the keyboard text font if +no OSK font is specified. + +The OSK font should not be used for a text view, because some +OSK fonts are appropriate for use only in the on screen keyboard; +see [`&displayMap`](/developer/language/reference/displaymap) for +reference. ## Description From 8fdbf04ce85ce12a8f9cdba4ddfd03ed9d42bf85 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Fri, 3 Jul 2026 17:26:05 +0200 Subject: [PATCH 13/14] fix(android): rename method to `getKeyboardTextFontFilenameOnly` This addresses a code review comment. --- .../java/com/tavultesoft/kmapro/WebBrowserActivity.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/WebBrowserActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/WebBrowserActivity.java index e7bda5b94e9..4c24da2559d 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/WebBrowserActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/WebBrowserActivity.java @@ -387,7 +387,7 @@ protected void onResume() { super.onResume(); if (webView != null) { if (didFinishLoading) { - String fontFilename = getKeyboardTextFontFilename(); + String fontFilename = getKeyboardTextFontFilenameOnly(); if (!loadedFont.equals(fontFilename)) { webView.reload(); } @@ -436,7 +436,7 @@ public void onBackPressed() { /** * Returns the filename without path of the display font of the current keyboard. */ - private String getKeyboardTextFontFilename() { + private String getKeyboardTextFontFilenameOnly() { String fontPath = KMManager.getKeyboardTextFontFilename(); if (fontPath == null || fontPath.isEmpty()) { return ""; @@ -445,7 +445,7 @@ private String getKeyboardTextFontFilename() { } private void loadFont() { - String font = getKeyboardTextFontFilename(); + String font = getKeyboardTextFontFilenameOnly(); if (!font.isEmpty()) { loadedFont = font; String fontUrl = String.format("%s%s", fontBaseUri, font); From dc47e32b87846e86a73bb18a23f3d1863a60ec80 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Wed, 8 Jul 2026 11:43:15 +0200 Subject: [PATCH 14/14] fix(android): address code review comments Co-authored-by: Marc Durdin --- .../java/com/keyman/engine/KMKeyboard.java | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) 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 a07d4a83b04..9f9895f5e60 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 @@ -792,10 +792,20 @@ private void sendError(String packageID, String keyboardID, String languageID, b } } + /** + * Return the root URL for the data folder. Even though this is a local + * location this returns a URL with a magic domain so that it can be + * loaded with fetch() in the webview. + */ private String getDataRootUrl() { return WebViewUtils.buildAssetUrl(""); } + /** + * Return the root path for the data folder as a file path. This should be + * used where the file is not loaded through the webview, but is instead + * used by the app directly. + */ private String getDataRootPath() { return context.getDir("data", Context.MODE_PRIVATE).toString() + File.separator; } @@ -913,15 +923,13 @@ private String getFontFilename(String font, String packageID) { return ""; } - if (FileUtils.hasFontExtension(font)) { - String fontRoot = KMManager.isDefaultFont(font) ? getDataRootPath() : getPackageRootPath(packageID); - return fontRoot + font; + if (!FileUtils.hasFontExtension(font)) { + // QUESTION: do we log this? + return ""; } - // REVIEW: Do we have to do anything if font is a JSONObject? - // See makeFontObj. - - return ""; + String fontRoot = KMManager.isDefaultFont(font) ? getDataRootPath() : getPackageRootPath(packageID); + return fontRoot + font; } @SuppressLint("InflateParams")