Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

change(android): smoother keyboard initialization ✨ #10022

Merged
merged 3 commits into from
Dec 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions android/KMEA/app/src/main/assets/android-host.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ function init() {
keyman.getOskHeight = getOskHeight;
keyman.getOskWidth = getOskWidth;
keyman.beepKeyboard = beepKeyboard;

// Readies the keyboard stub for instant loading during the init process.
KeymanWeb.registerStub(JSON.parse(jsInterface.initialKeyboard()));
Copy link
Contributor Author

@jahorton jahorton Nov 22, 2023

Choose a reason for hiding this comment

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

Uses the function defined within this PR within the Java KMKeyboardJSHandler class. See other comment (re https://developer.apple.com/documentation/webkit/wkscriptmessagehandlerwithreply) on how this could look for iOS if we wanted to go that route.

That said, I don't see the problem this PR addresses on my personal iOS device. So, adopting this code for use on iOS can probably be low-priority for now.


keyman.init({
'embeddingApp':device,
'fonts':'packages/',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,10 +243,8 @@ public boolean onConsoleMessage(ConsoleMessage cm) {
}

// Send console errors to Sentry in case they're missed by KMW sentryManager
// (Ignoring spurious message "No keyboard stubs exist = ...")
// TODO: Fix base error rather than trying to ignore it "No keyboard stubs exist"

if ((cm.messageLevel() == ConsoleMessage.MessageLevel.ERROR) && (!cm.message().startsWith("No keyboard stubs exist"))) {
if (cm.messageLevel() == ConsoleMessage.MessageLevel.ERROR) {
// Make Toast notification of error and send log about falling back to default keyboard (ignore language ID)
// Sanitize sourceId info
String NAVIGATION_PATTERN = "^(.*)?(keyboard\\.html#[^-]+)-.*$";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.keyman.engine;

import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
Expand All @@ -20,6 +21,7 @@
import static android.content.Context.VIBRATOR_SERVICE;

import com.keyman.engine.KMManager.KeyboardType;
import com.keyman.engine.data.Keyboard;
import com.keyman.engine.util.CharSequenceUtil;
import com.keyman.engine.util.KMLog;

Expand Down Expand Up @@ -62,6 +64,21 @@ public int getKeyboardWidth() {
return kbWidth;
}

@JavascriptInterface
public String initialKeyboard() {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

If we wanted to adopt something kinda similar for iOS:

https://developer.apple.com/documentation/webkit/wkscriptmessagehandlerwithreply

We could then reply with a stub string like the one this method builds, passing it through that Swift function's 'reply' callback for use and interpretation. (See the other comment in this batch.)

But... this is only available with iOS 14.0+.

// Note: KMManager.getCurrentKeyboard() (and similar) will throw errors until the host-page is first fully
// loaded and has set a keyboard. To allow the host-page to have earlier access, we instead get the stored
// keyboard index directly.
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 kbd = KMManager.getKeyboardInfo(this.context, index);
return kbd.toStub(context);
}

// This annotation is required in Jelly Bean and later:
@JavascriptInterface
public void beepKeyboard() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -747,12 +747,10 @@ public static void onConfigurationChanged(Configuration newConfig) {
// KMKeyboard
if (InAppKeyboard != null) {
RelativeLayout.LayoutParams params = getKeyboardLayoutParams();
InAppKeyboard.setLayoutParams(params);
InAppKeyboard.onConfigurationChanged(newConfig);
}
if (SystemKeyboard != null) {
RelativeLayout.LayoutParams params = getKeyboardLayoutParams();
SystemKeyboard.setLayoutParams(params);
SystemKeyboard.onConfigurationChanged(newConfig);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@
import com.keyman.engine.util.KMLog;
import com.keyman.engine.util.KMString;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.File;
import java.io.Serializable;
import java.util.ArrayList;

public class Keyboard extends LanguageResource implements Serializable {
private static final String TAG = "Keyboard";
Expand Down Expand Up @@ -171,6 +173,91 @@ public JSONObject toJSON() {
return o;
}

private String getKeyboardRoot(Context context) {
String keyboardRoot = context.getDir("data", Context.MODE_PRIVATE).toString() +
File.separator;

if (packageID.equals(KMManager.KMDefault_UndefinedPackageID)) {
return keyboardRoot + KMManager.KMDefault_UndefinedPackageID + File.separator;
} else {
return keyboardRoot + KMManager.KMDefault_AssetPackages + File.separator + packageID + File.separator;
}
}

public String getKeyboardPath(Context context) {
String keyboardID = this.getKeyboardID();
String keyboardVersion = this.getVersion();
if (packageID.equals(KMManager.KMDefault_UndefinedPackageID)) {
return getKeyboardRoot(context) + keyboardID + "-" + keyboardVersion + ".js";
} else {
return getKeyboardRoot(context) + keyboardID + ".js";
}
}

public String toStub(Context context) {
JSONObject stubObj = new JSONObject();

try {
stubObj.put("KN", this.getKeyboardName());
stubObj.put("KI", "Keyboard_" + this.getKeyboardID());
stubObj.put("KLC", this.getLanguageID());
stubObj.put("KL", this.getLanguageName());
stubObj.put("KF", this.getKeyboardPath(context));
stubObj.put("KP", this.getPackageID());

String displayFont = this.getFont();
if(displayFont != null) {
stubObj.put("KFont", this.buildDisplayFontObject(displayFont, context));
}

String oskFont = this.getOSKFont();
if(oskFont != null) {
stubObj.put("KOskFont", this.buildDisplayFontObject(oskFont, context));
}

String displayName = this.getDisplayName();
if(displayName != null) {
stubObj.put("displayName", displayName);
}

return stubObj.toString();
} catch(JSONException e) {
KMLog.LogException(TAG, "", e);
return null;
}
}

/**
* 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`
*/
private JSONObject buildDisplayFontObject(String font, Context context) {
if(font == null || font.equals("")) {
return null;
}

String keyboardRoot = this.getKeyboardRoot(context);

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

/**
* Get the fallback keyboard. If never specified, use sil_euro_latin
* @param context Context
Expand Down
18 changes: 12 additions & 6 deletions web/src/engine/main/src/keyboardInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,21 @@ export default class KeyboardInterface<ContextManagerType extends ContextManager
//
// The mobile apps typically have fully-preconfigured paths, but Developer's
// test-host page does not.
const pathConfig = this.engine.config.paths;
const stub = new KeyboardStub(Pstub, pathConfig.keyboards, pathConfig.fonts);
if(this.engine.keyboardRequisitioner?.cache.findMatchingStub(stub)) {
return 1;
}

const buildStub = () => {
const pathConfig = this.engine.config.paths;
return new KeyboardStub(Pstub, pathConfig.keyboards, pathConfig.fonts);
};

if(!this.engine.config.deferForInitialization.hasFinalized) {
this.engine.config.deferForInitialization.then(() => this.engine.keyboardRequisitioner.cache.addStub(stub));
// pathConfig is not ready until KMW initializes, which prevents proper stub-building.
this.engine.config.deferForInitialization.then(() => this.engine.keyboardRequisitioner.cache.addStub(buildStub()));
} else {
const stub = buildStub();

if(this.engine.keyboardRequisitioner?.cache.findMatchingStub(stub)) {
return 1;
}
this.engine.keyboardRequisitioner.cache.addStub(stub);
}

Expand Down