From 1191e4387f1060db9fee07498bad86e0a1d5cef9 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Thu, 2 Jul 2026 18:22:40 +0200 Subject: [PATCH 1/4] 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 | 121 ++++++++---------- .../java/com/keyman/engine/KMManager.java | 10 ++ .../docs/engine/KMManager/getFontTypeface.md | 9 +- .../KMManager/getKeyboardOskFontFilename.md | 12 +- .../KMManager/getKeyboardTextFontFilename.md | 12 +- 6 files changed, 97 insertions(+), 87 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 3308caff9a..e7bda5b94e 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 @@ public class WebBrowserActivity extends BaseActivity { 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 class WebBrowserActivity extends BaseActivity { } } + /** + * 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 541e20edcb..9a03909ab4 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; @@ -551,7 +549,7 @@ final class KMKeyboard extends WebView { * @return String */ public static String textFontFilename() { - return txtFont; + return txtFontPath; } /** @@ -559,10 +557,10 @@ final class KMKeyboard extends WebView { * @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 @@ -648,8 +646,6 @@ final class KMKeyboard extends WebView { } 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. @@ -704,16 +700,14 @@ final class KMKeyboard extends WebView { 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); @@ -810,26 +804,30 @@ final class KMKeyboard extends WebView { } } - // Set the base path of the keyboard depending on the package ID - private void setPackageRoot(String packageID) { - this.dataRoot = WebViewUtils.buildAssetUrl(""); + private String getDataRootUrl() { + return WebViewUtils.buildAssetUrl(""); + } + + private String getDataRootPath() { + return context.getDir("data", Context.MODE_PRIVATE).toString() + File.separator; + } + + private String getPackageRootUrl(String packageID) { if (packageID.equals(KMManager.KMDefault_UndefinedPackageID)) { - this.packageRoot = this.dataRoot + KMManager.KMDefault_UndefinedPackageID + "/"; - } else { - this.packageRoot = this.dataRoot + KMManager.KMDefault_AssetPackages + "/" + packageID + "/"; + return getDataRootUrl() + KMManager.KMDefault_UndefinedPackageID + "/"; } + return getDataRootUrl() + KMManager.KMDefault_AssetPackages + "/" + packageID + "/"; } - private String getDataRoot() { - return this.dataRoot; - } - - private String getPackageRoot() { - return this.packageRoot; + 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 { @@ -917,42 +915,25 @@ final class KMKeyboard extends WebView { } /** - * 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; - } - 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 = ""; + private String getFontFilename(String font, String packageID) { + if(font == null || font.equals("")) { + return ""; } - return font; + if (FileUtils.hasFontExtension(font)) { + String fontRoot = KMManager.isDefaultFont(font) ? getDataRootPath() : getPackageRootPath(packageID); + return fontRoot + font; + } + + // REVIEW: Do we have to do anything if font is a JSONObject? + // See makeFontObj. + + return ""; } @SuppressLint("InflateParams") @@ -1061,12 +1042,14 @@ final class KMKeyboard extends WebView { * 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; @@ -1077,12 +1060,16 @@ final class KMKeyboard extends WebView { 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" @@ -1094,7 +1081,7 @@ final class KMKeyboard extends WebView { 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) { @@ -1103,7 +1090,7 @@ final class KMKeyboard extends WebView { 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 3ca6f53de0..e46041fddb 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 @@ -1629,6 +1629,16 @@ public final class KMManager { */ 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 4149d92b36..5de7def6bd 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 8465ea4e8c..8644130a3c 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 5c381cc23a..f1e26e51a3 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 1867a7659fa361ca5e30afe40703adc46b64666d Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Fri, 3 Jul 2026 17:23:19 +0200 Subject: [PATCH 2/4] 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 5de7def6bd..9dd86928e7 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 8644130a3c..7406effb9d 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 70282bfd5b111850e2aa49c5d43cde5f2f8beec5 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Fri, 3 Jul 2026 17:26:05 +0200 Subject: [PATCH 3/4] 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 e7bda5b94e..4c24da2559 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 @@ public class WebBrowserActivity extends BaseActivity { super.onResume(); if (webView != null) { if (didFinishLoading) { - String fontFilename = getKeyboardTextFontFilename(); + String fontFilename = getKeyboardTextFontFilenameOnly(); if (!loadedFont.equals(fontFilename)) { webView.reload(); } @@ -436,7 +436,7 @@ public class WebBrowserActivity extends BaseActivity { /** * 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 @@ public class WebBrowserActivity extends BaseActivity { } private void loadFont() { - String font = getKeyboardTextFontFilename(); + String font = getKeyboardTextFontFilenameOnly(); if (!font.isEmpty()) { loadedFont = font; String fontUrl = String.format("%s%s", fontBaseUri, font); From 71aefb8f475f3e12ce42eaade4872fac6e974b2c Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Wed, 8 Jul 2026 11:43:15 +0200 Subject: [PATCH 4/4] 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 9a03909ab4..212de6d0a0 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 @@ -804,10 +804,20 @@ final class KMKeyboard extends WebView { } } + /** + * 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; } @@ -925,15 +935,13 @@ final class KMKeyboard extends WebView { 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")