Skip to content

fix(android): fix loading of keyboards on Android#16146

Open
ermshiperete wants to merge 1 commit into
masterfrom
fix/android/16096_blankKeyboard
Open

fix(android): fix loading of keyboards on Android#16146
ermshiperete wants to merge 1 commit into
masterfrom
fix/android/16096_blankKeyboard

Conversation

@ermshiperete

@ermshiperete ermshiperete commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

This PR migrates the Android WebView from loading local files via file:// URLs to using WebViewAssetLoader, which serves internal storage files through a magic HTTPS domain (https://appassets.androidplatform.net). This fixes the blank keyboard problem reported in #16096 for Android.

Core pattern of the change:

// Before:
"file://" + context.getDir("data", ...)
  + "/" + filename
// After:
"https://appassets.androidplatform.net"
  + "/data/" + filename

The WebViewAssetLoader in KMKeyboardWebViewClient intercepts requests to this domain and serves files from internal storage. Since the domain is a constant, Context is no longer needed to construct URLs, simplifying several method signatures.

Also this PR makes KMKeyboard.getKeyboardRoot() private, and renames public Keyboard.getKeyboardPath() to private Keyboard.getKeyboardUrl().

Part-of: #16096
Replaces: #16132

@github-project-automation github-project-automation Bot moved this to Todo in Keyman Jun 26, 2026
@keymanapp-test-bot keymanapp-test-bot Bot added the user-test-missing User tests have not yet been defined for the PR label Jun 26, 2026
@keymanapp-test-bot

keymanapp-test-bot Bot commented Jun 26, 2026

Copy link
Copy Markdown

User Test Results

Test specification and instructions

ERROR: user tests have not yet been defined

Test Artifacts

  • Android
    • Keyman for Android apk - build : all tests passed (no artifacts on BuildLevel "build")
    • FirstVoices Keyboards for Android apk - build : all tests passed (no artifacts on BuildLevel "build")
    • FirstVoices Keyboards for Android apk (old PRs) - build : all tests passed (no artifacts on BuildLevel "build")
    • KeyboardHarness apk - build : all tests passed (no artifacts on BuildLevel "build")
    • Keyman for Android apk (old PRs) - build : all tests passed (no artifacts on BuildLevel "build")
    • KMSample1 apk - build : all tests passed (no artifacts on BuildLevel "build")
    • KMSample2 apk - build : all tests passed (no artifacts on BuildLevel "build")

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
@ermshiperete ermshiperete force-pushed the fix/android/16096_blankKeyboard branch from b406759 to 64abd5b Compare June 26, 2026 18:02
@ermshiperete ermshiperete marked this pull request as ready for review June 26, 2026 18:10
@jahorton

Copy link
Copy Markdown
Contributor

Also this PR makes KMKeyboard.getKeyboardRoot() private, and renames public Keyboard.getKeyboardPath() to private Keyboard.getKeyboardUrl().

Note that KMKeyboard is a part of our public API for Keyman Engine for Android. That said, .getKeyboardRoot() was never published. It may be worth seeing if Scripture App Builder or Keyboard App Builder were using it.

https://help.keyman.com/developer/engine/android/current-version/KMKeyboard/

@jahorton jahorton left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM aside from the one nit.

}

public String getKeyboardRoot() {
private String getKeyboardRoot() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably leave this unchanged; KMKeyboard is an engine API class.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I asked the App Builder team about it, just in case; no effect for them at present.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was an internal public, not a published API endpoint, can safely make private.

@mcdurdin

Copy link
Copy Markdown
Member

Note that KMKeyboard is a part of our public API for Keyman Engine for Android.

If it's not documented, then it's not part of the API. It's a public method but an internal implementation detail.

@mcdurdin mcdurdin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking good, some tidyup and consolidation requested. Also have noted future work on KMDefault_UndefinedPackageID (we can turn that into a follow-up issue).

Comment thread android/KMEA/app/src/main/assets/keyboard.html
Comment on lines 179 to 181
if (packageID.equals(KMManager.KMDefault_UndefinedPackageID)) {
return keyboardRoot + KMManager.KMDefault_UndefinedPackageID + File.separator;
keyboardRoot += KMManager.KMDefault_UndefinedPackageID + "/";
} else {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this isn't new, I really wonder about the whole KMDefault_UndefinedPackageID (aka "cloud") pattern throughout this code. It seems like this is a fake "null" but I haven't fully audited its use. I would love to eliminate it altogether (28 references) because it seems like it is unnecessary complexity.

private static String txtFont = "";
private static String oskFont = null;
private static String keyboardRoot = "";
private String keyboardRoot = "";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why no longer static?

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 + "/";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
this.keyboardRoot = WebViewUtils.MAGIC_DEFAULT_DOMAIN + "/data/" + KMManager.KMDefault_UndefinedPackageID + "/";
this.keyboardRoot = WebViewUtils.buildAssetUrl(KMManager.KMDefault_UndefinedPackageID + "/");

Can we make a method under WebViewUtils which builds these URLs so we don't repeat the WebViewUtils.MAGIC_DEFAULT_DOMAIN + "/data/" pattern?

public String buildAssetUrl(String assetPath) {
  return WebViewUtils.MAGIC_DEFAULT_DOMAIN + "/data/" + assetPath;
}

Or even:

Suggested change
this.keyboardRoot = WebViewUtils.MAGIC_DEFAULT_DOMAIN + "/data/" + KMManager.KMDefault_UndefinedPackageID + "/";
this.keyboardRoot = WebViewUtils.buildAssetUrl([KMManager.KMDefault_UndefinedPackageID, ""]);
public String buildAssetUrl(String[] assetPath) {
  return WebViewUtils.MAGIC_DEFAULT_DOMAIN + "/data/" + String.join("/", assetPath);
}

} 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 + "/";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
this.keyboardRoot = WebViewUtils.MAGIC_DEFAULT_DOMAIN + "/data/" + KMManager.KMDefault_AssetPackages + "/" + packageID + "/";
this.keyboardRoot = WebViewUtils.buildAssetUrl(KMManager.KMDefault_AssetPackages + "/" + packageID + "/");

or

Suggested change
this.keyboardRoot = WebViewUtils.MAGIC_DEFAULT_DOMAIN + "/data/" + KMManager.KMDefault_AssetPackages + "/" + packageID + "/";
this.keyboardRoot = WebViewUtils.buildAssetUrl([KMManager.KMDefault_AssetPackages, packageID]);

Comment on lines +401 to +403
public static String getResourceUrl() {
return WebViewUtils.MAGIC_DEFAULT_DOMAIN +"/data/";
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use shared method in WebViewUtils instead as proposed

}

public static String getLexicalModelsUrl() {
return getResourceUrl() + KMDefault_LexicalModelPackages + "/";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return getResourceUrl() + KMDefault_LexicalModelPackages + "/";
return WebViewUtils.buildAssetUrl(KMDefault_LexicalModelPackages + "/");

or

Suggested change
return getResourceUrl() + KMDefault_LexicalModelPackages + "/";
return WebViewUtils.buildAssetUrl([KMDefault_LexicalModelPackages, ""]);

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;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
String htmlPath = WebViewUtils.MAGIC_DEFAULT_DOMAIN + "/data/" + KMManager.KMFilename_KeyboardHtml;
String htmlPath = WebViewUtils.buildAssetUrl(KMManager.KMFilename_KeyboardHtml);

or

Suggested change
String htmlPath = WebViewUtils.MAGIC_DEFAULT_DOMAIN + "/data/" + KMManager.KMFilename_KeyboardHtml;
String htmlPath = WebViewUtils.buildAssetUrl([KMManager.KMFilename_KeyboardHtml]);

Comment on lines +371 to +372
// Use the reserved magic domain for loading the keyboard from the local device.
// See https://developer.android.com/reference/androidx/webkit/WebViewAssetLoader

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment can just go in WebViewUtils.java, buildAssertUrl()

* internal storage path.
* See https://developer.android.com/reference/androidx/webkit/WebViewAssetLoader
*/
public static final String MAGIC_DEFAULT_DOMAIN = "https://appassets.androidplatform.net";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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 String buildAssetUrl(String assetPath) {
return WebViewUtils.MAGIC_DEFAULT_DOMAIN + WebViewUtils.ASSET_DATA_PATH + assetPath;
}

or

Suggested change
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, joining all components with '/'
*/
public String buildAssetUrl(String[] assetPath) {
return WebViewUtils.MAGIC_DEFAULT_DOMAIN + WebViewUtils.ASSET_DATA_PATH + String.join("/", assetPath);
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

android/app/ android/engine/ android/ fix user-test-missing User tests have not yet been defined for the PR

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

3 participants