Merge branch 'change/web/multi-token-correction-search' into change/web/start-multi-tokenization
Some checks failed
Keyman Build Summary / Summarize build status checks (push) Has been cancelled

This commit is contained in:
Joshua Horton 2026-08-03 14:06:04 -05:00
commit 0106fbdaee
452 changed files with 7518 additions and 4006 deletions

View file

@ -71,7 +71,7 @@ jobs:
- name: "Verify API for libkeymancore*.so (${{ steps.environment_step.outputs.GIT_BRANCH }}, branch ${{ steps.environment_step.outputs.GIT_BASE_BRANCH }}, by ${{ steps.environment_step.outputs.GIT_USER }})"
if: steps.environment_step.outputs.SKIP_API_CHECK != 'true'
run: |
echo "Verify API for libkeymancore*.so (${{ steps.environment_step.outputs.GIT_BRANCH }}, branch ${{ steps.environment_step.outputs.GIT_BASE_BRANCH }}, by ${{ steps.environment_step.outputs.GIT_USER }}):" >> $GITHUB_STEP_SUMMARY
echo "Verify API for libkeymancore*.so (${{ steps.environment_step.outputs.GIT_BRANCH }}, branch ${{ steps.environment_step.outputs.GIT_BASE_BRANCH }}, by ${{ steps.environment_step.outputs.GIT_USER }}):" | tee -a ${GITHUB_STEP_SUMMARY}
BIN_PACKAGE=$(ls "${GITHUB_WORKSPACE}/artifacts/" | grep "${PKG_NAME}[0-9]*_${{ steps.environment_step.outputs.KEYMAN_VERSION }}-1${{ steps.environment_step.outputs.PRERELEASE_TAG }}+$(lsb_release -c -s)1_amd64.deb")
cd ${{ github.workspace }}/keyman/linux
@ -80,7 +80,7 @@ jobs:
--bin-pkg "${GITHUB_WORKSPACE}/artifacts/${BIN_PACKAGE}" \
--git-sha "${{ steps.environment_step.outputs.GIT_SHA }}" \
--git-base "${{ steps.environment_step.outputs.GIT_BASE }}" \
verify 2>> $GITHUB_STEP_SUMMARY
verify 2> >(tee -a ${GITHUB_STEP_SUMMARY} >&2)
- name: Archive .symbols file
if: steps.environment_step.outputs.SKIP_API_CHECK != 'true' && always()

View file

@ -119,7 +119,7 @@ jobs:
- name: Install meson
shell: bash
run: |
pip3 install --user meson==1.10.1
pip3 install --user meson==1.11.0
- name: Run test
shell: bash

View file

@ -137,7 +137,7 @@ jobs:
strategy:
fail-fast: true
matrix:
dist: [jammy, noble, questing]
dist: [jammy, noble, resolute]
steps:
- name: Checkout

View file

@ -8,12 +8,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Update labels based on changed files
uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
- name: Update labels based on PR title
id: labeler
uses: fuxingloh/multi-labeler@f5bd7323b53b0833c1e4ed8d7b797ae995ef75b4 # v2.0.1
uses: fuxingloh/multi-labeler@bcd50af464202999e57f556b4aefcf05a34abf85 # v5.0.0
with:
github-token: ${{secrets.GITHUB_TOKEN}}
config-path: .github/multi-labeler.yml

4
.gitignore vendored
View file

@ -186,3 +186,7 @@ lcov.info
# flag file for build script
.configured
# see common/tools/api-extractor/README.md
tsdoc.json
api-extractor.json

View file

@ -1,5 +1,121 @@
# Keyman Version History
## 19.0.258 alpha 2026-07-15
* chore(android): remove doc for non-existing deprecated functions (#16244)
## 19.0.257 alpha 2026-07-14
* chore(linux): remove questing, add stonking (#16236)
* chore(android): cleanup Android build scripts and artifact filenames (#16234)
* chore(web): declare scope for stubAndKeyboardCache members (#16221)
* fix(android): improve clarity of keyboard script error popup (#16228)
* chore(android): log legacy cloud keyboards (#16237)
* chore(android): cleanup font variable names, remove obsolete code (#16211)
## 19.0.256 alpha 2026-07-13
* fix(android): add permissions for Sentry and set default keyboard (#16216)
* maint(resources): add extra debug reporting for builder (#16230)
## 19.0.255 alpha 2026-07-11
* maint(android): use KEYMAN_TIER instead of TIER.md (#16219)
* feat(ios): add keyman-version to package-version check (#16138)
* docs(web): cleanup of index pages and page titles (#16171)
* feat(android): add keyman-version to package-version check (#16139)
## 19.0.254 alpha 2026-07-10
* fix(android): handle blank keyboard tap properly (#16217)
* fix(web): define IME interfaces in `KeyboardInterfaceBase` (#16170)
## 19.0.253 alpha 2026-07-09
* fix(web): resync model in `set osk()` (as used by Server) (#16127)
* chore(developer): consolidate api-extractor usage in Developer (#16135)
## 19.0.252 alpha 2026-07-08
* chore(linux): reorder buttons on kmp install window (#16210)
* refactor(web): rename `keymanweb` to `keyman` in UI modules (#16174)
* chore: remove deprecated "X-UA-Compatible" and "apple-mobile-web-app-capable" metas (#16176)
* fix(android): refactor `KMLog` - simplification and added resilience (#16152)
* fix(android): differentiate between fontPath and fontUrl in Keyman Engine for Android (#16188)
* fix(web): fixes cookie unit-tests and path for OSK resources during testing (#16209)
## 19.0.251 alpha 2026-07-02
* fix(android): skip test on Windows that triggers androidx WebKit bug (#16184)
## 19.0.250 alpha 2026-07-01
* fix(android): use `WebViewAssetLoader` for loading assets on Android (address regression in 19.0.242) (#16146)
* fix(ios): bundle in KMW's globe-hint.css (#16142)
* fix(ios): load host page, keyboards through a consistent WKURLSchemeHandler (#16136)
## 19.0.249 alpha 2026-06-30
* fix(windows): add update property to remote check (#16126)
* chore(android): allow to build FV app in docker container (#16163)
* fix(developer): warn only on race when destroying TAppSourceHttpResponder (#16140)
* fix(developer): map shift key nextlayer property when importing OSK (#16110)
## 19.0.248 alpha 2026-06-29
* chore: add missing line to history (#16154)
* maint(resources): DRY out `PRInformation` interface (#16158)
* fix(developer): prevent clone of legacy keyboards with no source (#16111)
* fix(developer): consolidate user options in TypeScript code (#16134)
* fix(developer): fixup references to layer after deleting in Touch Layout Editor (#16129)
* fix(web): revert regression in setting `activeKeyboard` in `set osk` (#16156)
* test(web): add e2e tests for examples from guide (#16108)
* maint(linux): show output of API check also in log file (#16153)
## 19.0.247 alpha 2026-06-16
* chore: update multi-labeler to 5.0.0 (#16100)
## 19.0.246 alpha 2026-06-15
* feat(developer): run project validation from IDE (#16076)
* fix(core): use meson subsystem and needs_exe_wrapper for cross builds on Windows (#16088)
* docs(windows): add context documentation for additional Advanced options (#16089)
## 19.0.245 alpha 2026-06-10
* chore(web): hide KMX keyboard test page (#16083)
* docs(web): remove unnecessary mentioning of Keyman 17 from docs (#16085)
* chore(web): remove outdated `*_load.js` files (#16086)
* fix(web): fix `KeyboardStub.validateForCustomKeyboard` (#16067)
## 19.0.244 alpha 2026-06-09
* refactor(web): extract function, rename variables (#16063)
* refactor(web): reformatting of files (#16066)
* chore(web): improve the KeymanWeb integration documentation (#16064)
* chore(web): more improvements to documentation (#16070)
## 19.0.243 alpha 2026-06-08
* fix(developer): reduce km_core_keyboard_attrs size to 12 (#16075)
* chore(deps): bump minimatch in /core/tests/unit/wasm (#16045)
* chore(deps): bump glob from 10.4.5 to 10.5.0 in /core/tests/unit/wasm (#16046)
* chore(deps-dev): bump js-yaml from 4.1.0 to 4.2.0 in /core/tests/unit/wasm (#16047)
* chore(deps-dev): bump picomatch from 2.3.1 to 2.3.2 in /core/tests/unit/wasm (#16048)
* chore(deps): bump qs and express (#16009)
## 19.0.242 alpha 2026-06-03
* fix(web): fix displaying of keyboard menu (#16061)
## 19.0.241 alpha 2026-06-02
* chore: web-core preflight (#16015)
* chore(web): web-core preflight - strip core references (#16040)
* docs: add note on how to use composer on dockerized websites (#16029)
* fix(web): fix race displaying active keyboard in menu (#16042)
## 19.0.240 alpha 2026-05-28
* chore(web): add filename to link text of manual web tests (#16019)

View file

@ -1 +1 @@
19.0.241
19.0.259

View file

@ -1,5 +1,6 @@
#!/usr/bin/env bash
# Build Keyman for Android app (KMAPro)
# Keyman is copyright (C) SIL Global. MIT License.
#
## START STANDARD BUILD SCRIPT INCLUDE
# adjust relative paths as necessary
THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
@ -9,111 +10,92 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
. "$KEYMAN_ROOT/resources/build/utils.inc.sh"
. "$KEYMAN_ROOT/resources/build/build-help.inc.sh"
. "$KEYMAN_ROOT/resources/build/build-download-resources.sh"
. "$KEYMAN_ROOT/android/KMAPro/build-play-store-notes.inc.sh"
# ################################ Main script ################################
# Definition of global compile constants
CONFIG="release"
BUILD_FLAGS="build -x lint -x test" # Gradle build w/o test
TEST_FLAGS="-x assembleRelease lintRelease testRelease" # Gradle test w/o build
DAEMON_FLAG=
builder_describe "Builds Keyman for Android app." \
"@/android/KMEA" \
"clean" \
"configure" \
"build" \
"test Runs lint and unit tests." \
"publish-symbols Publishes symbols to Sentry." \
"test Runs lint and unit tests." \
"publish-symbols Publishes symbols to Sentry." \
"publish-play-store Publishes the APK to the Play Store."
# parse before describe_outputs to check debug flags
builder_parse "$@"
if builder_is_debug_build; then
builder_heading "### Debug config ####"
CONFIG="debug"
BUILD_FLAGS="assembleDebug -x lintDebug -x testDebug"
TEST_FLAGS="-x assembleDebug lintDebug testDebug"
else
BUILD_FLAGS="build -x lint -x test" # Gradle build w/o test
TEST_FLAGS="-x assembleRelease lintRelease testRelease" # Gradle test w/o build
fi
if builder_is_ci_build; then
GRADLE_DAEMON=--no-daemon
else
GRADLE_DAEMON=
fi
# Locations for bundled keyboard and lexical model
KEYBOARD_PACKAGE_ID="sil_euro_latin"
KEYBOARDS_TARGET="android/KMAPro/kMAPro/src/main/assets/${KEYBOARD_PACKAGE_ID}.kmp"
MODEL_PACKAGE_ID="nrc.en.mtnt"
MODELS_TARGET="android/KMAPro/kMAPro/src/main/assets/${MODEL_PACKAGE_ID}.model.kmp"
builder_describe_outputs \
configure "/${MODELS_TARGET}" \
build /android/KMAPro/kMAPro/build/outputs/apk/$CONFIG/keyman-${KEYMAN_VERSION_FOR_FILENAME}.apk
#### Build
# Parse args
if builder_is_ci_build; then
DAEMON_FLAG=--no-daemon
fi
build /android/KMAPro/kMAPro/build/outputs/apk/$BUILDER_CONFIGURATION/keyman-${KEYMAN_VERSION_FOR_FILENAME}.apk
#### Build action definitions ####
# Check about cleaning artifact paths
if builder_start_action clean; then
rm -rf "$KEYMAN_ROOT/android/KMAPro/kMAPro/build/outputs"
builder_finish_action success clean
fi
if builder_start_action configure; then
do_configure() {
downloadKeyboardPackage "$KEYBOARD_PACKAGE_ID" "${KEYMAN_ROOT}/$KEYBOARDS_TARGET"
downloadModelPackage "$MODEL_PACKAGE_ID" "${KEYMAN_ROOT}/$MODELS_TARGET"
}
builder_finish_action success configure
fi
if builder_start_action build; then
do_build() {
# Copy Keyman Engine for Android
cp "${KEYMAN_ROOT}/android/KMEA/app/build/outputs/aar/keyman-engine-${CONFIG}.aar" "${KEYMAN_ROOT}/android/KMAPro/kMAPro/libs/keyman-engine.aar"
cp "${KEYMAN_ROOT}/android/KMEA/app/build/outputs/aar/keyman-engine-${BUILDER_CONFIGURATION}.aar" "${KEYMAN_ROOT}/android/KMAPro/kMAPro/libs/keyman-engine.aar"
# Convert markdown to html for offline help
build_help_html android KMAPro/kMAPro/src/main/assets/info
echo "BUILD_FLAGS $BUILD_FLAGS"
./gradlew $DAEMON_FLAG clean $BUILD_FLAGS
builder_echo "BUILD_FLAGS $BUILD_FLAGS"
./gradlew $GRADLE_DAEMON $BUILD_FLAGS
mv "${KEYMAN_ROOT}/android/KMAPro/kMAPro/build/outputs/apk/$CONFIG/keyman-${KEYMAN_VERSION}.apk" "${KEYMAN_ROOT}/android/KMAPro/kMAPro/build/outputs/apk/$CONFIG/keyman-${KEYMAN_VERSION_FOR_FILENAME}.apk"
mv "${KEYMAN_ROOT}/android/KMAPro/kMAPro/build/outputs/apk/$BUILDER_CONFIGURATION/keyman-${KEYMAN_VERSION}.apk" "${KEYMAN_ROOT}/android/KMAPro/kMAPro/build/outputs/apk/$BUILDER_CONFIGURATION/keyman-${KEYMAN_VERSION_FOR_FILENAME}.apk"
}
builder_finish_action success build
fi
do_test() {
builder_echo "TEST_FLAGS $TEST_FLAGS"
./gradlew $GRADLE_DAEMON $TEST_FLAGS
}
if builder_start_action test; then
echo "TEST_FLAGS $TEST_FLAGS"
./gradlew $DAEMON_FLAG $TEST_FLAGS
builder_finish_action success test
fi
publish_symbols() {
do_publish_symbols() {
if builder_is_ci_build && builder_is_ci_build_level_release; then
# TODO: what does publishSentry even do?
./gradlew $DAEMON_FLAG publishSentry
./gradlew $GRADLE_DAEMON publishSentry
builder_echo "Making a Sentry release for tag $KEYMAN_VERSION_GIT_TAG"
sentry-cli upload-dif -p keyman-android --include-sources
sentry-cli releases -p keyman-android files $KEYMAN_VERSION_GIT_TAG upload-sourcemaps ./
fi
}
publish_play_store() {
do_publish_play_store() {
if builder_is_ci_build && builder_is_ci_build_level_release; then
generateReleaseNotes
# Publish Keyman for Android to Play Store
./gradlew $DAEMON_FLAG publishReleaseApk
./gradlew $GRADLE_DAEMON publishReleaseApk
fi
}
builder_run_action publish-symbols publish_symbols
builder_run_action publish-play-store publish_play_store
builder_run_action clean rm -rf "$KEYMAN_ROOT/android/KMAPro/kMAPro/build"
builder_run_action configure do_configure
builder_run_action build do_build
builder_run_action test do_test
builder_run_action publish-symbols do_publish_symbols
builder_run_action publish-play-store do_publish_play_store

View file

@ -8,8 +8,7 @@ plugins {
ext.rootPath = '../../'
apply from: "$rootPath/version.gradle"
String tier = new File('../../TIER.md').getText('UTF-8').trim();
String tier = System.getenv("KEYMAN_TIER");
java {
toolchain {
@ -173,10 +172,11 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
implementation 'com.google.android.material:material:1.12.0'
implementation 'com.stepstone.stepper:material-stepper:4.3.1'
implementation 'androidx.webkit:webkit:1.14.0'
implementation files('libs/keyman-engine.aar')
implementation 'io.sentry:sentry-android:8.19.1'
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,

View file

@ -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 = getKeyboardTextFontFilenameOnly();
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 getKeyboardTextFontFilenameOnly() {
String fontPath = KMManager.getKeyboardTextFontFilename();
if (fontPath == null || fontPath.isEmpty()) {
return "";
}
return new File(fontPath).getName();
}
private void loadFont() {
String font = KMManager.getKeyboardTextFontFilename();
String font = getKeyboardTextFontFilenameOnly();
if (!font.isEmpty()) {
loadedFont = font;
String fontUrl = String.format("%s%s", fontBaseUri, font);

View file

@ -8,7 +8,7 @@ ext.rootPath = '../../'
apply from: "$rootPath/version.gradle"
base {
// Specify library filename keyman-engine-$CONFIG.aar
// Specify library filename keyman-engine-$BUILDER_CONFIGURATION.aar
archivesName = "keyman-engine"
}
@ -72,6 +72,7 @@ dependencies {
implementation 'commons-io:commons-io:2.16.1'
implementation 'io.sentry:sentry-android:8.19.1'
implementation 'androidx.preference:preference:1.2.1'
implementation 'androidx.webkit:webkit:1.14.0'
// Robolectric
testImplementation 'androidx.test.ext:junit:1.2.1'

View file

@ -22,10 +22,6 @@ sentryManager.init();
window.addEventListener('load', init, false);
function loadDefaultKeyboard() {
notifyHost('reloadAfterError');
}
function init() {
//document.body.style.backgroundColor="transparent";
//window.console.log('Device type = '+device);
@ -82,7 +78,19 @@ function init() {
keyman.addEventListener('keyboardchange', setIsChiral);
keyman.core.languageProcessor.on('statechange', onStateChange);
document.body.addEventListener('touchend', loadDefaultKeyboard);
// If the keyboard fails to display, this usually indicates that a Javascript
// error has occurred, perhaps in the active keyboard. So we have a last-gasp
// fallback in that situation; if the user touches the blank document, we will
// attempt to reload the default keyboard.
document.addEventListener('touchend', (event) => {
if(event.target == document.documentElement) {
// The error will be reported through to Sentry if the user has error
// reporting enabled; breadcrumbs can give us helpful details on possible
// root causes for the error
window.console.error('user touched the document body, which can only happen if the keyboard was not presented. This is usually caused by another error.');
notifyHost('reloadAfterError');
}
});
notifyHost('pageLoaded');
}

View file

@ -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.DependencyUtil.LibraryType;
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;
@ -83,9 +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 keyboardRoot = "";
private static String textFontPath = "";
private static String oskFontPath = "";
private final String fontUndefined = "undefined";
private GestureDetector gestureDetector;
private static ArrayList<OnKeyboardEventListener> kbEventListeners = null;
@ -310,7 +310,7 @@ final class KMKeyboard extends WebView {
// This duplicates the sendKMWError message, which itself duplicates the reporting now
// managed by sentry-manager on the js side in patch in #6890. It does not give us
// additional useful information. So we don't re-send to Sentry.
sendError(packageID, keyboardID, "", false);
sendError(packageID, keyboardID, "", null);
}
return true;
@ -366,8 +366,7 @@ final class KMKeyboard extends WebView {
} else {
KMManager.SystemKeyboardWebViewClient.setKeyboardLoaded(false);
}
String htmlPath = "file://" + getContext().getDir("data", Context.MODE_PRIVATE) + "/" + KMManager.KMFilename_KeyboardHtml;
String htmlPath = WebViewUtils.buildAssetUrl(KMManager.KMFilename_KeyboardHtml);
loadUrl(htmlPath);
setBackgroundColor(0);
}
@ -550,7 +549,7 @@ final class KMKeyboard extends WebView {
* @return String
*/
public static String textFontFilename() {
return txtFont;
return textFontPath;
}
/**
@ -558,9 +557,10 @@ final class KMKeyboard extends WebView {
* @return String
*/
public static String oskFontFilename() {
return oskFont;
return oskFontPath;
}
// 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
@ -601,7 +601,11 @@ final class KMKeyboard extends WebView {
}
if (!KMManager.shouldAllowSetKeyboard() || kbInfo == null) {
sendError(packageID, keyboardID, languageID, true);
if(!KMManager.shouldAllowSetKeyboard()) {
sendError(packageID, keyboardID, languageID, "setKeyboard.short failed with shouldAllowSetKeyboard == false");
} else {
sendError(packageID, keyboardID, languageID, "setKeyboard.short failed with kbInfo == null");
}
kbInfo = KeyboardController.getInstance().getKeyboardInfo(0);
retVal = false;
} else {
@ -632,7 +636,7 @@ final class KMKeyboard extends WebView {
if (!KMManager.shouldAllowSetKeyboard() ||
(packageID.equals(KMManager.KMDefault_UndefinedPackageID) && keyboardVersion == null)) {
sendError(packageID, keyboardID, languageID, true);
sendError(packageID, keyboardID, languageID, "prepareKeyboardSwitch");
Keyboard kbInfo = KeyboardController.getInstance().getKeyboardInfo(0);
packageID = kbInfo.getPackageID();
keyboardID = kbInfo.getKeyboardID();
@ -646,8 +650,6 @@ final class KMKeyboard extends WebView {
}
String kbKey = KMString.format("%s_%s", languageID, keyboardID);
setKeyboardRoot(packageID);
// Escape single-quoted names for javascript call
keyboardName = keyboardName.replaceAll("\'", "\\\\'"); // Double-escaped-backslash b/c regex.
@ -662,15 +664,15 @@ final class KMKeyboard extends WebView {
}
public boolean setKeyboard(String packageID, String keyboardID, String languageID,
String keyboardName, String languageName, String kFont,
String kOskFont) {
String keyboardName, String languageName, String textFontFilename,
String oskFontFilename) {
return setKeyboard(packageID, keyboardID, languageID, keyboardName, languageName,
kFont, kOskFont, null);
textFontFilename, oskFontFilename, null);
}
public boolean setKeyboard(String packageID, String keyboardID, String languageID,
String keyboardName, String languageName, String kFont,
String kOskFont, String displayName) {
String keyboardName, String languageName, String textFontFilename,
String oskFontFilename, String displayName) {
if (packageID == null || keyboardID == null || languageID == null || keyboardName == null || languageName == null) {
return false;
}
@ -686,15 +688,19 @@ final class KMKeyboard extends WebView {
if (!KMManager.shouldAllowSetKeyboard() ||
(packageID.equals(KMManager.KMDefault_UndefinedPackageID) && keyboardVersion == null)) {
sendError(packageID, keyboardID, languageID, true);
if(!KMManager.shouldAllowSetKeyboard()) {
sendError(packageID, keyboardID, languageID, "setKeyboard.full failed with shouldAllowSetKeyboard == false");
} else {
sendError(packageID, keyboardID, languageID, "setKeyboard.full failed with packageID == KMDefault_UndefinedPackageID and keyboardVersion == null");
}
Keyboard kbInfo = KeyboardController.getInstance().getKeyboardInfo(0);
packageID = kbInfo.getPackageID();
keyboardID = kbInfo.getKeyboardID();
languageID = kbInfo.getLanguageID();
keyboardName = kbInfo.getKeyboardName();
languageName = kbInfo.getLanguageName();
kFont = kbInfo.getFont();
kOskFont = kbInfo.getOSKFont();
textFontFilename = kbInfo.getFont();
oskFontFilename = kbInfo.getOSKFont();
retVal = false;
// Keyboard changed, so determine version again
@ -702,20 +708,18 @@ final class KMKeyboard extends WebView {
KMManager.getLatestKeyboardFileVersion(getContext(), packageID, keyboardID) : null;
}
setKeyboardRoot(packageID);
if(oskFontFilename == null || oskFontFilename.isEmpty())
oskFontFilename = textFontFilename;
if(kOskFont == null || kOskFont.isEmpty())
kOskFont = kFont;
JSONObject textFont = makeFontObject(textFontFilename, packageID);
JSONObject oskFont = makeFontObject(oskFontFilename, packageID);
JSONObject jDisplayFont = makeFontPaths(kFont);
JSONObject jOskFont = makeFontPaths(kOskFont);
txtFont = getFontFilename(jDisplayFont);
oskFont = getFontFilename(jOskFont);
textFontPath = getFontFilename(textFontFilename, packageID);
oskFontPath = getFontFilename(oskFontFilename, packageID);
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 {
@ -723,11 +727,11 @@ final class KMKeyboard extends WebView {
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);
if (jOskFont != null) reg.put("KOskFont", jOskFont);
if (textFont != null) reg.put("KFont", textFont);
if (oskFont != null) reg.put("KOskFont", oskFont);
if (displayName != null) reg.put("displayName", displayName);
} catch(JSONException e) {
KMLog.LogException(TAG, "", e);
@ -784,53 +788,68 @@ final class KMKeyboard extends WebView {
}
public boolean getChirality() {
return this.isChiral;
}
// Display localized Toast notification that keyboard selection failed, so loading default keyboard.
// Also sends a message to Sentry (not localized)
private void sendError(String packageID, String keyboardID, String languageID, boolean reportToSentry) {
/**
* Display localized Toast notification that keyboard selection failed, so
* loading default keyboard. Also sends a message to Sentry (not localized)
*/
private void sendError(String packageID, String keyboardID, String languageID, String sentryMessage) {
this.currentKeyboardErrorReports++;
if(this.currentKeyboardErrorReports == 1) {
BaseActivity.makeToast(context, R.string.fatal_keyboard_error_short, Toast.LENGTH_LONG, packageID, keyboardID, languageID);
BaseActivity.makeToast(context, R.string.fatal_keyboard_error, Toast.LENGTH_LONG, packageID, keyboardID, languageID);
}
if(this.currentKeyboardErrorReports < 5 && DependencyUtil.libraryExists(LibraryType.SENTRY) && Sentry.isEnabled() && reportToSentry) {
// We'll only report up to 5 errors in a given keyboard to avoid spamming
// errors and using unnecessary bandwidth doing so
// Don't use localized string R.string.fatal_keyboard_error msg for Sentry
String msg = KMString.format("Error in keyboard %1$s:%2$s for %3$s language.",
packageID, keyboardID, languageID);
if(this.currentKeyboardErrorReports < 5 && sentryMessage != null && DependencyUtil.libraryExists(LibraryType.SENTRY) && Sentry.isEnabled()) {
// We'll only report up to 5 errors in a given keyboard to avoid spamming errors and using unnecessary bandwidth doing so
String msg = KMString.format("Script error in keyboard webview\nContext: %4$s\nActive package: %1$s\nActive keyboard: %2$s\nActive language: %3$s",
packageID, keyboardID, languageID, sentryMessage);
Sentry.captureMessage(msg);
}
}
// 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);
} else {
this.keyboardRoot = (context.getDir("data", Context.MODE_PRIVATE).toString() +
File.separator + KMManager.KMDefault_AssetPackages + File.separator + packageID + File.separator);
}
/**
* 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("");
}
public String getKeyboardRoot() {
return this.keyboardRoot;
/**
* 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;
}
private String makeKeyboardPath(String packageID, String keyboardID, String keyboardVersion) {
String keyboardPath;
private String getPackageRootUrl(String packageID) {
if (packageID.equals(KMManager.KMDefault_UndefinedPackageID)) {
keyboardPath = getKeyboardRoot() + keyboardID + "-" + keyboardVersion + ".js";
} else {
keyboardPath = getKeyboardRoot() + keyboardID + ".js";
return getDataRootUrl() + KMManager.KMDefault_UndefinedPackageID + "/";
}
return keyboardPath;
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 = getPackageRootUrl(packageID);
if (packageID.equals(KMManager.KMDefault_UndefinedPackageID)) {
keyboardUrl += keyboardID + "-" + keyboardVersion + ".js";
} else {
keyboardUrl += keyboardID + ".js";
}
return keyboardUrl;
}
private void sendKMWError(int lineNumber, String sourceId, String message) {
@ -912,42 +931,23 @@ 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 fontFilename String - Font filename
* @param packageID String - Package ID
* @return String - Full path to the font file. If fontFilename 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 fontFilename, String packageID) {
if(fontFilename == null || fontFilename.equals("")) {
return "";
}
return font;
if (!FileUtils.hasFontExtension(fontFilename)) {
// QUESTION: do we log this?
return "";
}
String fontRoot = KMManager.isDefaultFont(fontFilename) ? getDataRootPath() : getPackageRootPath(packageID);
return fontRoot + fontFilename;
}
@SuppressLint("InflateParams")
@ -1042,13 +1042,28 @@ final class KMKeyboard extends WebView {
}
/**
* 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
* @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 makeFontPaths(String font) {
private JSONObject makeFontObject(String font, String packageID) {
if(font == null || font.equals("")) {
return null;
@ -1059,14 +1074,17 @@ final class KMKeyboard extends WebView {
JSONObject jfont = new JSONObject();
jfont.put(KMManager.KMKey_FontFamily, font.substring(0, font.length()-4));
JSONArray jfiles = new JSONArray();
jfiles.put(keyboardRoot + font);
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);
JSONArray sourceArray;
String fontFile;
// Replace "sources" key with "files"
if (fontObj.has(KMManager.KMKey_FontSource)) {
@ -1076,16 +1094,18 @@ final class KMKeyboard extends WebView {
Object obj = fontObj.get(KMManager.KMKey_FontFiles);
if (obj instanceof String) {
fontFile = fontObj.getString(KMManager.KMKey_FontFiles);
fontObj.put(KMManager.KMKey_FontFiles, keyboardRoot + obj);
String fontFile = fontObj.getString(KMManager.KMKey_FontFiles);
String fontRoot = KMManager.isDefaultFont(fontFile) ? getDataRootUrl() : getPackageRootUrl(packageID);
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, keyboardRoot + fontFile);
String fontRoot = KMManager.isDefaultFont(fontFile) ? getDataRootUrl() : getPackageRootUrl(packageID);
fontObj.put(KMManager.KMKey_FontFiles, fontRoot + fontFile);
fontObj.remove(KMManager.KMKey_FontSource);
return fontObj;
}
@ -1094,7 +1114,6 @@ final class KMKeyboard extends WebView {
}
} catch (JSONException e) {
KMLog.LogException(TAG, "Failed to make font for '"+font+"'", e);
return null;
}
return null;

View file

@ -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.Bitmap;
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;
@ -19,6 +23,7 @@ import com.keyman.engine.KMManager.KeyboardType;
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;
@ -31,11 +36,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(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) {
KMLog.LogError(TAG, String.format("Cannot initialize: Invalid keyboard type: %s", keyboardType.toString()));
@ -58,6 +68,11 @@ public final class KMKeyboardWebViewClient extends WebViewClient {
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 +89,44 @@ public final class KMKeyboardWebViewClient extends WebViewClient {
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);
Keyboard keyboardInfo = null;
if (index >= 0) {
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();
}
}

View file

@ -1,5 +1,5 @@
/**
* Copyright (C) 2017 SIL International. All rights reserved.
/*
* Keyman is copyright (C) SIL Global. MIT License.
*/
package com.keyman.engine;
@ -406,6 +406,10 @@ public final class KMManager {
return getResourceRoot() + KMDefault_LexicalModelPackages + File.separator;
}
public static String getLexicalModelsUrl() {
return WebViewUtils.buildAssetUrl(KMDefault_LexicalModelPackages + "/");
}
public static String getCloudDir() {
return getResourceRoot() + KMDefault_UndefinedPackageID + File.separator;
}
@ -496,6 +500,19 @@ public final class KMManager {
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 DejaVuSans.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();
@ -1612,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) &&
@ -1650,8 +1677,9 @@ public final class KMManager {
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()) {
@ -1666,7 +1694,7 @@ public final class KMManager {
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);
@ -2225,7 +2253,12 @@ public final class KMManager {
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";
}

View file

@ -45,7 +45,7 @@ public class CloudRepository {
public static final String API_STAGING_HOST = "api.keyman.com"; // #7227 disabling: "api.keyman-staging.com";
public static final String API_MODEL_LANGUAGE_FORMATSTR = "https://%s/model?q=bcp47:%s";
public static final String API_PACKAGE_VERSION_FORMATSTR = "https://%s/package-version?platform=android%s%s";
public static final String API_PACKAGE_VERSION_FORMATSTR = "https://%s/package-version?platform=android&keyman-version=%s%s%s";
private Dataset memCachedDataset;
private Calendar lastLoad; // To be used for Dataset caching.
@ -205,7 +205,7 @@ public class CloudRepository {
}
}
String queryURL = String.format(API_PACKAGE_VERSION_FORMATSTR, getHost(), keyboardQuery, lexicalModelQuery);
String queryURL = String.format(API_PACKAGE_VERSION_FORMATSTR, getHost(), KMManager.getVersion(), keyboardQuery, lexicalModelQuery);
return new CloudApiTypes.CloudApiParam(
CloudApiTypes.ApiTarget.PackageVersion, queryURL).setType(CloudApiTypes.JSONType.Object);
}

View file

@ -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.BCP47;
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;
@ -48,6 +49,7 @@ public class Keyboard extends LanguageResource implements Serializable {
*/
public Keyboard(JSONObject installedObj) {
this.fromJSON(installedObj);
logIfLegacyKeyboard();
}
/**
@ -78,6 +80,8 @@ public class Keyboard extends LanguageResource implements Serializable {
this.helpLink = keyboardJSON.optString(KMManager.KMKey_CustomHelpLink,
KMString.format(HELP_URL_FORMATSTR, HELP_URL_HOST, this.resourceID, this.version));
logIfLegacyKeyboard();
} catch (JSONException e) {
KMLog.LogException(TAG, "Keyboard exception parsing JSON: ", e);
}
@ -95,6 +99,8 @@ public class Keyboard extends LanguageResource implements Serializable {
this.isNewKeyboard = isNewKeyboard;
this.font = (font != null) ? font : "";
this.oskFont = (oskFont != null) ? oskFont : "";
logIfLegacyKeyboard();
}
public Keyboard(Keyboard k) {
@ -107,6 +113,12 @@ public class Keyboard extends LanguageResource implements Serializable {
this.displayName = k.getDisplayName();
}
private void logIfLegacyKeyboard() {
if (this.packageID.equals(KMManager.KMDefault_UndefinedPackageID)) {
KMLog.LogInfo(TAG, "Constructing legacy keyboard: " + this.resourceID);
}
}
public String getKeyboardID() { return getResourceID(); }
public String getKeyboardName() { return getResourceName(); }
@ -173,24 +185,24 @@ public class Keyboard extends LanguageResource implements Serializable {
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;
}
private String getDataRoot() {
return WebViewUtils.buildAssetUrl("");
}
public String getKeyboardPath(Context context) {
private String getPackageRoot() {
if (packageID.equals(KMManager.KMDefault_UndefinedPackageID)) {
return getDataRoot() + KMManager.KMDefault_UndefinedPackageID + "/";
}
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(context) + keyboardID + "-" + keyboardVersion + ".js";
return getPackageRoot() + keyboardID + "-" + keyboardVersion + ".js";
} else {
return getKeyboardRoot(context) + keyboardID + ".js";
return getPackageRoot() + keyboardID + ".js";
}
}
@ -202,17 +214,17 @@ public class Keyboard extends LanguageResource implements Serializable {
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();
@ -228,34 +240,33 @@ public class Keyboard extends LanguageResource implements Serializable {
}
/**
* 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, Context context) {
private JSONObject buildDisplayFontObject(String font) {
if(font == null || font.equals("")) {
return null;
}
String keyboardRoot = this.getKeyboardRoot(context);
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;
}
/**

View file

@ -24,16 +24,11 @@ import io.sentry.SentryLevel;
public final class KMLog {
private static final String TAG = "KMLog";
private static final String KEYBOARD_TAG = "keyboardId";
private static final String KEYBOARD_COUNT_TAG = "installedKeyboardCount";
private static final String MODEL_TAG = "modelId";
private static final String LANGCODE_TAG = "languageCode";
// Some of the methods used to generate debug logging information can, themselves,
// trigger errors that can also trigger the same logging. We must not get
// caught in an infinite loop / stack-overflow; this field helps us avoid states
// that would otherwise cause error-looping, etc.
private static boolean isLogging = false;
private static final String KEYBOARD_TAG = "keyman.keyboardId";
private static final String KEYBOARD_COUNT_TAG = "keyman.installedKeyboardCount";
private static final String MODEL_TAG = "keyman.modelId";
private static final String LANGCODE_TAG = "keyman.languageCode";
private static final String DEBUG_LOGGING_ERROR_TAG = "keyman.debugLoggingError";
private static void tagDebugInfo() {
String kbdId = "";
@ -57,13 +52,16 @@ public final class KMLog {
}
}
} catch (Exception ex) {
String msg = ex.getMessage() == null ? "" : ex.getMessage();
Sentry.setExtra("debugLoggingError", msg);
Sentry.setTag(DEBUG_LOGGING_ERROR_TAG, ex.getMessage() == null ? "" : ex.getMessage());
}
Sentry.setExtra(KEYBOARD_TAG, kbdId);
Sentry.setExtra(KEYBOARD_COUNT_TAG, "" + kbdCount);
Sentry.setExtra(LANGCODE_TAG, lngCode);
Sentry.setExtra(MODEL_TAG, modelId);
Sentry.setTag(KEYBOARD_TAG, kbdId);
Sentry.setTag(KEYBOARD_COUNT_TAG, "" + kbdCount);
Sentry.setTag(LANGCODE_TAG, lngCode);
Sentry.setTag(MODEL_TAG, modelId);
}
private static boolean canLogToSentry() {
return DependencyUtil.libraryExists(LibraryType.SENTRY) && Sentry.isEnabled();
}
/**
@ -72,19 +70,20 @@ public final class KMLog {
* @param msg String of the info message
*/
public static void LogInfo(String tag, String msg) {
if(isLogging) {
if (msg == null || msg.isEmpty()) {
return;
}
isLogging = true;
if (msg != null && !msg.isEmpty()) {
Log.i(tag, msg);
if (DependencyUtil.libraryExists(LibraryType.SENTRY) && Sentry.isEnabled()) {
tagDebugInfo();
Sentry.captureMessage(msg, SentryLevel.INFO);
}
Log.i(tag, msg);
if (!canLogToSentry()) {
return;
}
isLogging = false;
Sentry.withScope(scope -> {
tagDebugInfo();
Sentry.captureMessage(msg, SentryLevel.INFO);
});
}
/**
@ -98,15 +97,9 @@ public final class KMLog {
return;
}
if(isLogging) {
return;
}
isLogging = true;
Log.i(tag, msg);
if (!DependencyUtil.libraryExists(LibraryType.SENTRY) || !Sentry.isEnabled()) {
isLogging = false;
if (!canLogToSentry()) {
return;
}
@ -114,27 +107,30 @@ public final class KMLog {
crumb.setMessage(msg);
crumb.setLevel(SentryLevel.INFO);
if(addStackTrace) {
StackTraceElement[] rawTrace = Thread.currentThread().getStackTrace();
try {
if(addStackTrace) {
StackTraceElement[] rawTrace = Thread.currentThread().getStackTrace();
// The call that gets us the stack-trace above... shows up in the
// stack trace, so we'll skip the first few (redundant) entries.
int skipCount = 3;
// The call that gets us the stack-trace above... shows up in the
// stack trace, so we'll skip the first few (redundant) entries.
int skipCount = 3;
// Sentry does limit the size of messages... so let's just
// keep 10 entries and call it a day.
int limit = Math.min(rawTrace.length, 10 + skipCount);
if(rawTrace.length > skipCount) {
String[] trace = new String[limit - skipCount];
for (int i = skipCount; i < limit; i++) {
trace[i-skipCount] = rawTrace[i].toString();
// Sentry does limit the size of messages... so let's just
// keep 10 entries and call it a day.
if(rawTrace.length > skipCount) {
int limit = Math.min(rawTrace.length, 10 + skipCount);
String[] trace = new String[limit - skipCount];
for (int i = skipCount; i < limit; i++) {
trace[i-skipCount] = rawTrace[i].toString();
}
crumb.setData("stacktrace", trace);
}
crumb.setData("stacktrace", trace);
}
} catch (Exception e) {
Sentry.captureException(e);
}
tagDebugInfo();
Sentry.addBreadcrumb(crumb);
isLogging = false;
}
/**
@ -143,23 +139,32 @@ public final class KMLog {
* @param msg String of the error message
*/
public static void LogError(String tag, String msg) {
if(isLogging) {
if (msg == null || msg.isEmpty()) {
return;
}
isLogging = true;
if (msg != null && !msg.isEmpty()) {
Log.e(tag, msg);
Log.e(tag, msg);
try {
// On alpha and beta tiers, we pop an error toast so testers can be aware
// of the error
if (KMManager.getTier(BuildConfig.KEYMAN_ENGINE_VERSION_NAME) != KMManager.Tier.STABLE) {
BaseActivity.makeToast(null, msg, Toast.LENGTH_LONG);
}
if (DependencyUtil.libraryExists(LibraryType.SENTRY) && Sentry.isEnabled()) {
tagDebugInfo();
Sentry.captureMessage(msg, SentryLevel.ERROR);
} catch(Exception e) {
if(canLogToSentry()) {
Sentry.captureException(e);
}
}
isLogging = false;
if (!canLogToSentry()) {
return;
}
Sentry.withScope(scope -> {
tagDebugInfo();
Sentry.captureMessage(msg, SentryLevel.ERROR);
});
}
/**
@ -169,28 +174,7 @@ public final class KMLog {
* @param e Throwable exception
*/
public static void LogException(String tag, String msg, Throwable e) {
if(isLogging) {
return;
}
isLogging = true;
String errorMsg = "";
if (msg != null && !msg.isEmpty()) {
errorMsg = msg + "\n" + e;
} else if (e != null) {
errorMsg = e.getMessage();
}
Log.e(tag, errorMsg, e);
if (KMManager.getTier(BuildConfig.KEYMAN_ENGINE_VERSION_NAME) != KMManager.Tier.STABLE) {
BaseActivity.makeToast(null, errorMsg, Toast.LENGTH_LONG);
}
if (DependencyUtil.libraryExists(LibraryType.SENTRY) && Sentry.isEnabled()) {
tagDebugInfo();
Sentry.addBreadcrumb(errorMsg);
Sentry.captureException(e);
}
isLogging = false;
KMLog.LogExceptionWithData(tag, msg, null, null, e);
}
/**
@ -203,25 +187,48 @@ public final class KMLog {
*/
public static void LogExceptionWithData(String tag, String msg,
String objName, Object obj, Throwable e) {
if(isLogging) {
String errorMsg = "";
try {
if (msg != null && !msg.isEmpty()) {
errorMsg = msg + "\n" + e;
} else if (e != null) {
errorMsg = e.toString();
}
// On alpha and beta tiers, we pop an error toast so testers can be aware
// of the exception
if (KMManager.getTier(BuildConfig.KEYMAN_ENGINE_VERSION_NAME) != KMManager.Tier.STABLE) {
BaseActivity.makeToast(null, errorMsg, Toast.LENGTH_LONG);
}
} catch (Exception innerE) {
if (canLogToSentry()) {
Sentry.captureException(innerE);
}
}
Log.e(tag, errorMsg);
if (!canLogToSentry()) {
return;
}
isLogging = true;
if (obj != null && DependencyUtil.libraryExists(LibraryType.SENTRY) && Sentry.isEnabled()) {
Sentry.addBreadcrumb(errorMsg);
Sentry.withScope(scope -> {
tagDebugInfo();
String objStr = null;
try {
objStr = obj.toString();
Sentry.setExtra(objName, objStr);
} catch (Exception innerE) {
LogException(TAG, "Sentry.setExtra failed for " + objName, innerE);
if(obj != null && objName != null) {
Sentry.setExtra(objName, obj.toString());
}
} catch(Exception innerE) {
Sentry.captureException(innerE);
}
// Report the original exception
LogException(tag, msg, e);
// And remove the exception-specific tagged data, lest it also be
// tracked on subsequent errors not associated with the current call.
Sentry.removeExtra(objName);
}
isLogging = false;
Sentry.captureException(e);
if(obj != null && objName != null) {
// And remove the exception-specific tagged data, lest it also be
// tracked on subsequent errors not associated with the current call.
Sentry.removeExtra(objName);
}
});
}
}

View file

@ -42,6 +42,28 @@ public final class WebViewUtils {
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
*/
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) {
String appendAsset = assetPath == null ? "" :
(assetPath.startsWith("/") ? assetPath.substring(1) : assetPath);
return WebViewUtils.MAGIC_DEFAULT_DOMAIN + WebViewUtils.ASSET_DATA_PATH + appendAsset;
}
/**
* Get the Keyman Engine mode based on the Chrome version.
* @param context - The context

View file

@ -80,8 +80,6 @@
<string name="fileprovider_undefined" comment="FileProvider library needed to view help file">የእገዛ ፋይልን ለማየት ፋይል ፕሮቫይደሩ ቤተ መጽሐፍት ያስፈልገዋል %1$s</string>
<!-- Context: Fatal keyboard error. Will load default keyboard -->
<string name="fatal_keyboard_error" comment="Fatal keyboard error (keyboard ID::packageID for language). Will load default keyboard">የገበታ ሰሌዳ ፋታል ችግር ዝርዝር %1$s:%2$s ለ %3$s ቋንቋ፤ መደበኛ ቋንቋ ጭኗል።</string>
<!-- Context: Fatal keyboard error. -->
<string name="fatal_keyboard_error_short" comment="Error in keyboard (keyboard ID::packageID for language).">የፊደል ገበታ ሰሌዳው ችግር %1$s:%2$s ለ %3$s ቋንቋ።</string>
<!-- Context: Query for associated dictionary -->
<string name="query_associated_model" comment="Check if there's an available dictionary to download">ተያያዥነት ያለውን መዝገበ ቃላት ለመጫን እየፈተሸ ነው።</string>
<string name="cannot_query_associated_model" comment="Cannot reach server to check if there's an available dictionary to download">የሚወርደውን መዝገበ ቃላት ከኪይማን ሰርቨር ጋር መገናኘት አልተቻለም</string>

View file

@ -88,8 +88,6 @@
<string name="fileprovider_undefined" comment="FileProvider library needed to view help file">مكتبة FileProvider مطلوبة لعرض ملفات المساعدة: %1$s</string>
<!-- Context: Fatal keyboard error. Will load default keyboard -->
<string name="fatal_keyboard_error" comment="Fatal keyboard error (keyboard ID::packageID for language). Will load default keyboard">خطأ كبير في لوحة مفاتيح مع %1$s:%2$s لأجل لغة %3$s. سيتم تحميل لوحة المفاتيح الأساسية.</string>
<!-- Context: Fatal keyboard error. -->
<string name="fatal_keyboard_error_short" comment="Error in keyboard (keyboard ID::packageID for language).">خطأ في لوحة مفاتيح %1$s:%2$s لأجل لغة %3$s.</string>
<!-- Context: Query for associated dictionary -->
<string name="query_associated_model" comment="Check if there's an available dictionary to download">يجري التحقق من قاموس ذي صلة للتحميل</string>
<string name="cannot_query_associated_model" comment="Cannot reach server to check if there's an available dictionary to download"> لا يمكن الاتصال بخادم Keyman للتحقق من تنزيل القاموس المرتبط</string>

View file

@ -80,8 +80,6 @@
<string name="fileprovider_undefined" comment="FileProvider library needed to view help file">FileProvider (Fayl Təminatçısı) kitabxanasına kömək sənədinə baxmaq lazımdır:%1$s</string>
<!-- Context: Fatal keyboard error. Will load default keyboard -->
<string name="fatal_keyboard_error" comment="Fatal keyboard error (keyboard ID::packageID for language). Will load default keyboard">%3$s dili üçün %1$s ilə ölümcül klaviatura xətası :%3$s dili üçün %2$s. Varsayılan klaviatura yüklənir.</string>
<!-- Context: Fatal keyboard error. -->
<string name="fatal_keyboard_error_short" comment="Error in keyboard (keyboard ID::packageID for language).">%3$s dil üçün %1$s:%2$s klaviaturasında xəta.</string>
<!-- Context: Query for associated dictionary -->
<string name="query_associated_model" comment="Check if there's an available dictionary to download">Yükləmək üçün əlaqəli lüğət yoxlanılır</string>
<string name="cannot_query_associated_model" comment="Cannot reach server to check if there's an available dictionary to download"> Cannot connect to Keyman server to check for associated dictionary to download</string>

View file

@ -80,8 +80,6 @@
<string name="fileprovider_undefined" comment="FileProvider library needed to view help file">Ἀπαιτεῖται βιβλιοθήκη FileProvider γιὰ νὰ δεῖτε ἀρχεῖο βοηθείας: %1$s</string>
<!-- Context: Fatal keyboard error. Will load default keyboard -->
<string name="fatal_keyboard_error" comment="Fatal keyboard error (keyboard ID::packageID for language). Will load default keyboard">Μοιραῖο σφάλμα πληκτρολογίου στὸ %1$s:%2$s γιὰ τὴν %3$s γλῶσσα. Φορτώνεται προεπιλεγμένο πληκτρολόγιο.</string>
<!-- Context: Fatal keyboard error. -->
<string name="fatal_keyboard_error_short" comment="Error in keyboard (keyboard ID::packageID for language).">Σφάλμα στὸ πληκτρολόγιο %1$s:%2$s γιὰ τὴν γλῶσσα \"%3$s\".</string>
<!-- Context: Query for associated dictionary -->
<string name="query_associated_model" comment="Check if there's an available dictionary to download">Ἀναζήτηση σχετικοῦ λεξικοῦ πρὸς μεταφόρτωσιν</string>
<string name="cannot_query_associated_model" comment="Cannot reach server to check if there's an available dictionary to download">Ἀδυναμία συνδέσεως μὲ τὸν διακομιστὴ Keyman γιὰ τὴν εὕρεση σχετικοῦ λεξικοῦ πρὸς μεταφόρτωσιν</string>

View file

@ -80,8 +80,6 @@
<string name="fileprovider_undefined" comment="FileProvider library needed to view help file"> Se necesita la librería FileProvider para ver el archivo de ayuda: %1$s</string>
<!-- Context: Fatal keyboard error. Will load default keyboard -->
<string name="fatal_keyboard_error" comment="Fatal keyboard error (keyboard ID::packageID for language). Will load default keyboard"> Error fatal del teclado con %1$s:%2$s para el idioma %3$s. Cargando teclado predeterminado.</string>
<!-- Context: Fatal keyboard error. -->
<string name="fatal_keyboard_error_short" comment="Error in keyboard (keyboard ID::packageID for language)."> Error en el teclado %1$s:%2$s para el idioma %3$s.</string>
<!-- Context: Alert Dialog body: System WebView not installed -->
<string name="body_install_webview" comment="Notification when WebView not installed"> Keyman requiere que WebView esté instalado.\"</string>
<!-- Context: Alert Dialog body: System WebView installed but not enabled -->

View file

@ -84,8 +84,6 @@
<string name="fileprovider_undefined" comment="FileProvider library needed to view help file">FilProvider library bindawar ley fakkakanal shu\'ul da: %1$s</string>
<!-- Context: Fatal keyboard error. Will load default keyboard -->
<string name="fatal_keyboard_error" comment="Fatal keyboard error (keyboard ID::packageID for language). Will load default keyboard">Mushkila kabirey bey %1$s:%2$s layl %3$s lugga. Dassasan shabal keyboard hanal gabul.</string>
<!-- Context: Fatal keyboard error. -->
<string name="fatal_keyboard_error_short" comment="Error in keyboard (keyboard ID::packageID for language).">Mushkila fyl keyboard %1$s:%2$s layl %3$s lugga.</string>
<!-- Context: Query for associated dictionary -->
<string name="query_associated_model" comment="Check if there's an available dictionary to download">Shafiyan kakkad abu kalimat shaba da ley dassasana</string>
<string name="cannot_query_associated_model" comment="Cannot reach server to check if there's an available dictionary to download"> Cannot connect to Keyman server to check for associated dictionary to download</string>

View file

@ -80,8 +80,6 @@
<string name="fileprovider_undefined" comment="FileProvider library needed to view help file"> Mda akwa bara library na ar FileProvider kamnyar ka mdǝ wutǝ fayil ar dlara: %1$s</string>
<!-- Context: Fatal keyboard error. Will load default keyboard -->
<string name="fatal_keyboard_error" comment="Fatal keyboard error (keyboard ID::packageID for language). Will load default keyboard"> Dametǝdzi na ar Fatal akwa keyboard ka %1$s:%2$s ala %3$s hulfur mnya. Akwa hara lodi ar default keyboard.</string>
<!-- Context: Fatal keyboard error. -->
<string name="fatal_keyboard_error_short" comment="Error in keyboard (keyboard ID::packageID for language)."> Dametǝdzi akwa keyboard %1$s:%2$s na ar %3$s hulfur mnya.</string>
<!-- Context: Query for associated dictionary -->
<string name="query_associated_model" comment="Check if there's an available dictionary to download">Akwa pakǝ dictionary na kǝɓa kari kamnyar katsa hara dawunlodi ni</string>
<string name="cannot_query_associated_model" comment="Cannot reach server to check if there's an available dictionary to download"> Cannot connect to Keyman server to check for associated dictionary to download</string>

View file

@ -80,8 +80,6 @@
<string name="fileprovider_undefined" comment="FileProvider library needed to view help file"> Gada bara library nǝr FileProvider akuyaka gǝ wul su akwa fail nǝr dlarɛ: %1$s</string>
<!-- Context: Fatal keyboard error. Will load default keyboard -->
<string name="fatal_keyboard_error" comment="Fatal keyboard error (keyboard ID::packageID for language). Will load default keyboard"> Fatal keyboard error with %1$s:%2$s for %3$s language. Loading default keyboard.</string>
<!-- Context: Fatal keyboard error. -->
<string name="fatal_keyboard_error_short" comment="Error in keyboard (keyboard ID::packageID for language)."> Su kura kal kalakwa kibod %1$s:%2$s nǝr %3$s nya.</string>
<!-- Context: Query for associated dictionary -->
<string name="query_associated_model" comment="Check if there's an available dictionary to download">Ata kǝra chakǝnta akuyaka ɓui suyere tara tsukwar bwar dictionary kadǝbar ka ndǝ download ko sǝgǝl akǝr</string>
<string name="cannot_query_associated_model" comment="Cannot reach server to check if there's an available dictionary to download"> Gadiyata mǝzi bui sava nǝr Keyman wɛ akuya ka gǝ bui ka sǝgǝl aka jiliyir dikshinari namtǝ gada bara. </string>

View file

@ -84,8 +84,6 @@
<string name="fileprovider_undefined" comment="FileProvider library needed to view help file"> Knihovna FileProvider potřebná k zobrazení nápovědy: %1$s</string>
<!-- Context: Fatal keyboard error. Will load default keyboard -->
<string name="fatal_keyboard_error" comment="Fatal keyboard error (keyboard ID::packageID for language). Will load default keyboard"> Fatální chyba klávesnice s %1$s:%2$s pro jazyk %3$s . Načítání výchozí klávesnice.</string>
<!-- Context: Fatal keyboard error. -->
<string name="fatal_keyboard_error_short" comment="Error in keyboard (keyboard ID::packageID for language)."> Chyba v klávesnici %1$s:%2$s pro jazyk %3$s.</string>
<!-- Context: Alert Dialog body: System WebView not installed -->
<string name="body_install_webview" comment="Notification when WebView not installed">Keyman vyžaduje, aby byl nainstalován WebView.“</string>
<!-- Context: Alert Dialog body: System WebView installed but not enabled -->

View file

@ -80,8 +80,6 @@
<string name="fileprovider_undefined" comment="FileProvider library needed to view help file"> FileProvider-Bibliothek zum Anzeigen der Hilfe-Datei: %1$s</string>
<!-- Context: Fatal keyboard error. Will load default keyboard -->
<string name="fatal_keyboard_error" comment="Fatal keyboard error (keyboard ID::packageID for language). Will load default keyboard"> Fataler Tastaturfehler mit %1$s:%2$s für %3$s Sprache. Standard-Tastatur wird geladen.</string>
<!-- Context: Fatal keyboard error. -->
<string name="fatal_keyboard_error_short" comment="Error in keyboard (keyboard ID::packageID for language)."> Fehler in Tastatur %1$s:%2$s für %3$s Sprache.</string>
<!-- Context: Alert Dialog body: System WebView not installed -->
<string name="body_install_webview" comment="Notification when WebView not installed"> Keyman benötigt die Installation von WebView.\"</string>
<!-- Context: Alert Dialog body: System WebView installed but not enabled -->

View file

@ -80,8 +80,6 @@
<string name="fileprovider_undefined" comment="FileProvider library needed to view help file"> Biblioteca de FileProvider necesaria para ver el archivo de ayuda: %1$s</string>
<!-- Context: Fatal keyboard error. Will load default keyboard -->
<string name="fatal_keyboard_error" comment="Fatal keyboard error (keyboard ID::packageID for language). Will load default keyboard"> Error de teclado fatal con %1$s:%2$s para %3$s idioma. Cargando teclado predeterminado.</string>
<!-- Context: Fatal keyboard error. -->
<string name="fatal_keyboard_error_short" comment="Error in keyboard (keyboard ID::packageID for language)."> Error en el teclado %1$s:%2$s para %3$s idioma.</string>
<!-- Context: Alert Dialog body: System WebView not installed -->
<string name="body_install_webview" comment="Notification when WebView not installed"> Keyman requiere que WebView esté instalado.\"</string>
<!-- Context: Alert Dialog body: System WebView installed but not enabled -->

View file

@ -80,8 +80,6 @@
<string name="fileprovider_undefined" comment="FileProvider library needed to view help file"> Babal fayel ɗo mari haaje laarugo fayel mballa: %1$s </string>
<!-- Context: Fatal keyboard error. Will load default keyboard -->
<string name="fatal_keyboard_error" comment="Fatal keyboard error (keyboard ID::packageID for language). Will load default keyboard"> Kibod man wari bee masala bee %1$s:%2$s jee %3$s ɗemngal. Ɗo nastina kibod ko naftirta. </string>
<!-- Context: Fatal keyboard error. -->
<string name="fatal_keyboard_error_short" comment="Error in keyboard (keyboard ID::packageID for language)."> Masala nder kibod %1$s:%2$s jee %3$s ɗemngal. </string>
<!-- Context: Query for associated dictionary -->
<string name="query_associated_model" comment="Check if there's an available dictionary to download">Ɗo tefa deftere ma`ana kalimaji ngam jippina</string>
<string name="cannot_query_associated_model" comment="Cannot reach server to check if there's an available dictionary to download"> Waawaa seŋaade e sarworde Keyman ngam ƴeewtaade so saggitorde yahdoonde heen ena woodi</string>

View file

@ -80,8 +80,6 @@
<string name="fileprovider_undefined" comment="FileProvider library needed to view help file">Deftordu FileProvider ena soklaa ngam yiyde fiilde ballal: %1$s</string>
<!-- Context: Fatal keyboard error. Will load default keyboard -->
<string name="fatal_keyboard_error" comment="Fatal keyboard error (keyboard ID::packageID for language). Will load default keyboard">Juumre tappirde halkoore e %1$s: %2$s mo ɗemngal %3$s. Nana loowa tappirde woowaande.</string>
<!-- Context: Fatal keyboard error. -->
<string name="fatal_keyboard_error_short" comment="Error in keyboard (keyboard ID::packageID for language)."> Juumre e tappirde %1$s:%2$s ɗemngal %3$s language.</string>
<!-- Context: Query for associated dictionary -->
<string name="query_associated_model" comment="Check if there's an available dictionary to download">Nana ƴeewtoo saggitorde yahdiinde ngam aawtaade</string>
<string name="cannot_query_associated_model" comment="Cannot reach server to check if there's an available dictionary to download">Waawaa seŋaade e sarworde Keyman ngam ƴeewtaade so saggitorde aawtoonde ena woodi</string>

View file

@ -80,8 +80,6 @@
<string name="fileprovider_undefined" comment="FileProvider library needed to view help file"> Librairie FileProvider nécessaire pour afficher le fichier d\'aide : %1$s</string>
<!-- Context: Fatal keyboard error. Will load default keyboard -->
<string name="fatal_keyboard_error" comment="Fatal keyboard error (keyboard ID::packageID for language). Will load default keyboard"> Erreur fatale de clavier avec %1$s:%2$s pour %3$s langue. Chargement du clavier par défaut.</string>
<!-- Context: Fatal keyboard error. -->
<string name="fatal_keyboard_error_short" comment="Error in keyboard (keyboard ID::packageID for language)."> Erreur dans le clavier %1$s:%2$s pour la langue %3$s.</string>
<!-- Context: Query for associated dictionary -->
<string name="query_associated_model" comment="Check if there's an available dictionary to download">Vérification du dictionnaire associé à télécharger</string>
<string name="cannot_query_associated_model" comment="Cannot reach server to check if there's an available dictionary to download"> Impossible de se connecter au serveur Keyman pour chercher un dictionnaire à télécharger</string>

View file

@ -80,8 +80,6 @@
<string name="fileprovider_undefined" comment="FileProvider library needed to view help file">Ana buƙatar wajen karatu na FileProvider domin duba fayil ɗin taimako: %1$s</string>
<!-- Context: Fatal keyboard error. Will load default keyboard -->
<string name="fatal_keyboard_error" comment="Fatal keyboard error (keyboard ID::packageID for language). Will load default keyboard">Matsalar madanni da %1$s:%2$s domin %3$s yaruka. Saukar da madanni mai kyau.</string>
<!-- Context: Fatal keyboard error. -->
<string name="fatal_keyboard_error_short" comment="Error in keyboard (keyboard ID::packageID for language).">Kuskure a yaren %1$s:%2$s for %3$s madanni.</string>
<!-- Context: Query for associated dictionary -->
<string name="query_associated_model" comment="Check if there's an available dictionary to download">Duba ƙamus mai alaƙa domin a sauke</string>
<string name="cannot_query_associated_model" comment="Cannot reach server to check if there's an available dictionary to download"> Cannot connect to Keyman server to check for associated dictionary to download</string>

View file

@ -82,8 +82,6 @@
<string name="fileprovider_undefined" comment="FileProvider library needed to view help file"> FileProvider library needed to view help file: %1$s</string>
<!-- Context: Fatal keyboard error. Will load default keyboard -->
<string name="fatal_keyboard_error" comment="Fatal keyboard error (keyboard ID::packageID for language). Will load default keyboard"> Fatal keyboard nda kuskure %1$s:%2$s anga %3$s gwaɗaha luwa. Kalt girgira keyboard.</string>
<!-- Context: Fatal keyboard error. -->
<string name="fatal_keyboard_error_short" comment="Error in keyboard (keyboard ID::packageID for language)."> Kuskure an keyboard %1$s:%2$s for %3$s gwaɗa luwa.</string>
<!-- Context: Query for associated dictionary -->
<string name="query_associated_model" comment="Check if there's an available dictionary to download">Naghata anga gwaft dictionary da sangata</string>
<string name="cannot_query_associated_model" comment="Cannot reach server to check if there's an available dictionary to download"> Cannot connect to Keyman server to check for associated dictionary to download</string>

View file

@ -78,8 +78,6 @@
<string name="fileprovider_undefined" comment="FileProvider library needed to view help file">Butuh FileProvider library untuk menampilkan berkas bantuan: %1$s</string>
<!-- Context: Fatal keyboard error. Will load default keyboard -->
<string name="fatal_keyboard_error" comment="Fatal keyboard error (keyboard ID::packageID for language). Will load default keyboard">Terjadi masalah dengan papan tombol %1$s:%2$s untuk bahasa %3$s. Papan tombol diubah ke setelah baku.</string>
<!-- Context: Fatal keyboard error. -->
<string name="fatal_keyboard_error_short" comment="Error in keyboard (keyboard ID::packageID for language)."> Error in keyboard %1$s:%2$s for %3$s language.</string>
<!-- Context: Query for associated dictionary -->
<string name="query_associated_model" comment="Check if there's an available dictionary to download">Mengecek kamus yang terkait untuk diunduh</string>
<string name="cannot_query_associated_model" comment="Cannot reach server to check if there's an available dictionary to download"> Cannot connect to Keyman server to check for associated dictionary to download</string>

View file

@ -80,8 +80,6 @@
<string name="fileprovider_undefined" comment="FileProvider library needed to view help file"> Libreria FileProvider necessaria per visualizzare il file d\'aiuto: %1$s</string>
<!-- Context: Fatal keyboard error. Will load default keyboard -->
<string name="fatal_keyboard_error" comment="Fatal keyboard error (keyboard ID::packageID for language). Will load default keyboard"> Errore fatale della tastiera con %1$s:%2$s per %3$s lingua. Caricamento tastiera predefinita.</string>
<!-- Context: Fatal keyboard error. -->
<string name="fatal_keyboard_error_short" comment="Error in keyboard (keyboard ID::packageID for language)."> Errore nella tastiera %1$s:%2$s per %3$s lingua.</string>
<!-- Context: Alert Dialog body: System WebView not installed -->
<string name="body_install_webview" comment="Notification when WebView not installed"> Keyman richiede che WebView sia installato.\"</string>
<!-- Context: Alert Dialog body: System WebView installed but not enabled -->

View file

@ -78,8 +78,6 @@
<string name="fileprovider_undefined" comment="FileProvider library needed to view help file"> ត្រូវការបណ្ណាល័យ FileProvider ដើម្បី​មើលឯកសារជំនួយ៖ %1$s</string>
<!-- Context: Fatal keyboard error. Will load default keyboard -->
<string name="fatal_keyboard_error" comment="Fatal keyboard error (keyboard ID::packageID for language). Will load default keyboard"> មាន​បញ្ហាធ្ងន់ធ្ងរចំពោះក្ដារចុច %1$s:%2$s សម្រាប់​ភាសា %3$s។ កំពុង​ផ្ទុក​ក្ដារចុច​លំនាំដើម។</string>
<!-- Context: Fatal keyboard error. -->
<string name="fatal_keyboard_error_short" comment="Error in keyboard (keyboard ID::packageID for language)."> បញ្ហាក្នុងក្ដារចុច %1$s:%2$s សម្រាប់ភាសា %3$s។</string>
<!-- Context: Alert Dialog body: System WebView not installed -->
<string name="body_install_webview" comment="Notification when WebView not installed">Keyman តម្រូវឱ្យដំឡើង WebView ។\"</string>
<!-- Context: Alert Dialog body: System WebView installed but not enabled -->

View file

@ -80,8 +80,6 @@
<string name="fileprovider_undefined" comment="FileProvider library needed to view help file">ಸಹಾಯ ಫೈಲ್ ವೀಕ್ಷಿಸಲು FileProvider ಲೈಬ್ರರಿ ಅಗತ್ಯವಿದೆ: %1$s</string>
<!-- Context: Fatal keyboard error. Will load default keyboard -->
<string name="fatal_keyboard_error" comment="Fatal keyboard error (keyboard ID::packageID for language). Will load default keyboard">%1$s:%2$s ಕೀಲಿಮಣೆಯಲ್ಲಿ %3$s ಭಾಷೆಯೊಂದಿಗೆ ದೋಷವಿದೆ. ಅದರ ಬದಲಿಗೆ ಪೂರ್ವನಿರ್ಧರಿತ ಕೀಲಿಮಣೆ ಲೋಡ್ ಆಗುತ್ತಿದೆ.</string>
<!-- Context: Fatal keyboard error. -->
<string name="fatal_keyboard_error_short" comment="Error in keyboard (keyboard ID::packageID for language).">%1$s:%2$s ಕೀಲಿಮಣೆಯಲ್ಲಿ %3$s ಭಾಷೆಯೊಂದಿಗೆ ದೋಷವಿದೆ.</string>
<!-- Context: Query for associated dictionary -->
<string name="query_associated_model" comment="Check if there's an available dictionary to download">ಸಂಬಂಧಿಸಿದ ಶಬ್ದಕೋಶವನ್ನು ಡೌನ್‌ಲೋಡ್ ಮಾಡಲು ಪರಿಶೀಲಿಸಲಾಗುತ್ತಿದೆ</string>
<string name="cannot_query_associated_model" comment="Cannot reach server to check if there's an available dictionary to download">ಸಂಬಂಧಿಸಿದ ಶಬ್ದಕೋಶವನ್ನು ಡೌನ್‌ಲೋಡ್ ಮಾಡಲು ಕೀಮ್ಯಾನ್ ಸರ್ವರ್ ಅನ್ನು ಸಂಪರ್ಕಿಸಲು ಸಾಧ್ಯವಾಗುತ್ತಿಲ್ಲಾ</string>

View file

@ -80,8 +80,6 @@
<string name="fileprovider_undefined" comment="FileProvider library needed to view help file"> FileProvider library needed to view help file: %1$s</string>
<!-- Context: Fatal keyboard error. Will load default keyboard -->
<string name="fatal_keyboard_error" comment="Fatal keyboard error (keyboard ID::packageID for language). Will load default keyboard"> Fatal keyboard error with %1$s:%2$s for %3$s language. Loading default keyboard.</string>
<!-- Context: Fatal keyboard error. -->
<string name="fatal_keyboard_error_short" comment="Error in keyboard (keyboard ID::packageID for language).">Taltǝ keyboard dǝn mbeji %1$s:%2$s for %3$s tǝlama.</string>
<!-- Context: Query for associated dictionary -->
<string name="query_associated_model" comment="Check if there's an available dictionary to download">Dictionary shiro kǝllata mane download nzǝ diye</string>
<string name="cannot_query_associated_model" comment="Cannot reach server to check if there's an available dictionary to download"> Cannot connect to Keyman server to check for associated dictionary to download</string>

View file

@ -80,8 +80,6 @@
<string name="fileprovider_undefined" comment="FileProvider library needed to view help file"> FileProvider library a kátá zhara file mla: %1$s</string>
<!-- Context: Fatal keyboard error. Will load default keyboard -->
<string name="fatal_keyboard_error" comment="Fatal keyboard error (keyboard ID::packageID for language). Will load default keyboard"> Kuskure Fatal keyboard antara%1$s:%2$s gé %3$s nare. femhe nde default keyboard.</string>
<!-- Context: Fatal keyboard error. -->
<string name="fatal_keyboard_error_short" comment="Error in keyboard (keyboard ID::packageID for language)."> Kuskure am keyboard %1$s:%2$s for %3$s nare.</string>
<!-- Context: Query for associated dictionary -->
<string name="query_associated_model" comment="Check if there's an available dictionary to download">Zharanahe dictionary harzhe gé tsukwanahe</string>
<string name="cannot_query_associated_model" comment="Cannot reach server to check if there's an available dictionary to download"> Cannot connect to Keyman server to check for associated dictionary to download</string>

View file

@ -78,8 +78,6 @@
<string name="fileprovider_undefined" comment="FileProvider library needed to view help file"> တၚ်နၚ်လိက် FileProvider မိက်ဂွံဗဵု ဝှါၚ်အရီုဗၚ်: %1$s</string>
<!-- Context: Fatal keyboard error. Will load default keyboard -->
<string name="fatal_keyboard_error" comment="Fatal keyboard error (keyboard ID::packageID for language). Will load default keyboard"> ကဳဗုဒ်ယောၚ်ယာဇၞော်ဇၞော်နကဵု %1$s:%2$s သွက် ဘာသာ %3$s ။ ဂြဲပ္တိုန်မံၚ် ကဳဗုဒ်ပကတိ။</string>
<!-- Context: Fatal keyboard error. -->
<string name="fatal_keyboard_error_short" comment="Error in keyboard (keyboard ID::packageID for language)."> တၚ်ယောၚ်ယာ ပ္ဍဲကဳဗုဒ် %1$s:%2$s သွက် ဘာသာ %3$s</string>
<!-- Context: Alert Dialog body: System WebView not installed -->
<string name="body_install_webview" comment="Notification when WebView not installed">သ္ဂောံဂြဲဖျေံ Keyman ဂှ် ဒးသုၚ်စောဲ WebView ရောၚ်။</string>
<!-- Context: Alert Dialog body: System WebView installed but not enabled -->

View file

@ -80,8 +80,6 @@
<string name="fileprovider_undefined" comment="FileProvider library needed to view help file">FileProvider library needed to view help file: %1$s</string>
<!-- Context: Fatal keyboard error. Will load default keyboard -->
<string name="fatal_keyboard_error" comment="Fatal keyboard error (keyboard ID::packageID for language). Will load default keyboard"> Fatal keyboard error with %1$s:%2$s for %3$s language. Loading default keyboard.</string>
<!-- Context: Fatal keyboard error. -->
<string name="fatal_keyboard_error_short" comment="Error in keyboard (keyboard ID::packageID for language).">Ŋya nur keyboard %1$s:%2$s anu %3$s kra kalkal.</string>
<!-- Context: Query for associated dictionary -->
<string name="query_associated_model" comment="Check if there's an available dictionary to download">Luba dictionary kuku nagu acittu downloadi</string>
<string name="cannot_query_associated_model" comment="Cannot reach server to check if there's an available dictionary to download"> Cannot connect to Keyman server to check for associated dictionary to download</string>

View file

@ -80,8 +80,6 @@
<string name="fileprovider_undefined" comment="FileProvider library needed to view help file"> FileProvider bibliotheek nodig om het hulpbestand te bekijken: %1$s</string>
<!-- Context: Fatal keyboard error. Will load default keyboard -->
<string name="fatal_keyboard_error" comment="Fatal keyboard error (keyboard ID::packageID for language). Will load default keyboard"> Fatale toetsenbord fout met %1$s:%2$s voor %3$s taal. Standaard toetsenbord wordt geladen.</string>
<!-- Context: Fatal keyboard error. -->
<string name="fatal_keyboard_error_short" comment="Error in keyboard (keyboard ID::packageID for language)."> Fout in toetsenbord %1$s:%2$s voor %3$s taal.</string>
<!-- Context: Query for associated dictionary -->
<string name="query_associated_model" comment="Check if there's an available dictionary to download">Downloaden van bijbehorende woordenboek</string>
<string name="cannot_query_associated_model" comment="Cannot reach server to check if there's an available dictionary to download"> Cannot connect to Keyman server to check for associated dictionary to download</string>

View file

@ -84,8 +84,6 @@
<string name="fileprovider_undefined" comment="FileProvider library needed to view help file"> Biblioteka FileProvider potrzebna, aby wyświetlić plik pomocy: %1$s</string>
<!-- Context: Fatal keyboard error. Will load default keyboard -->
<string name="fatal_keyboard_error" comment="Fatal keyboard error (keyboard ID::packageID for language). Will load default keyboard"> Błąd krytycznej klawiatury z %1$s:%2$s dla %3$s języka. Ładowanie domyślnej klawiatury.</string>
<!-- Context: Fatal keyboard error. -->
<string name="fatal_keyboard_error_short" comment="Error in keyboard (keyboard ID::packageID for language)."> Błąd w klawiaturze %1$s:%2$s dla %3$s języka.</string>
<!-- Context: Query for associated dictionary -->
<string name="query_associated_model" comment="Check if there's an available dictionary to download">Sprawdzanie dla skojarzonego słownika do pobrania</string>
<string name="cannot_query_associated_model" comment="Cannot reach server to check if there's an available dictionary to download"> Cannot connect to Keyman server to check for associated dictionary to download</string>

View file

@ -80,8 +80,6 @@
<string name="fileprovider_undefined" comment="FileProvider library needed to view help file"> Biblioteca de FileProvider necessária para ver o arquivo de ajuda: %1$s</string>
<!-- Context: Fatal keyboard error. Will load default keyboard -->
<string name="fatal_keyboard_error" comment="Fatal keyboard error (keyboard ID::packageID for language). Will load default keyboard"> Erro fatal de teclado com %1$s:%2$s para %3$s idioma. Carregando o teclado padrão.</string>
<!-- Context: Fatal keyboard error. -->
<string name="fatal_keyboard_error_short" comment="Error in keyboard (keyboard ID::packageID for language)."> Erro no teclado %1$s:%2$s para %3$s idioma.</string>
<!-- Context: Alert Dialog body: System WebView not installed -->
<string name="body_install_webview" comment="Notification when WebView not installed"> Keyman requer que o WebView seja instalado.\"</string>
<!-- Context: Alert Dialog body: System WebView installed but not enabled -->

View file

@ -84,8 +84,6 @@
<string name="fileprovider_undefined" comment="FileProvider library needed to view help file"> Библиотека файлового провайдера необходима для просмотра файла справки: %1$s</string>
<!-- Context: Fatal keyboard error. Will load default keyboard -->
<string name="fatal_keyboard_error" comment="Fatal keyboard error (keyboard ID::packageID for language). Will load default keyboard"> Неустранимая ошибка клавиатуры %1$s:%2$s для %3$s языка. Загрузка по умолчанию клавиатуры.</string>
<!-- Context: Fatal keyboard error. -->
<string name="fatal_keyboard_error_short" comment="Error in keyboard (keyboard ID::packageID for language)."> Ошибка в клавиатуре %1$s:%2$s для языка %3$s.</string>
<!-- Context: Query for associated dictionary -->
<string name="query_associated_model" comment="Check if there's an available dictionary to download">Проверка соответствующего словаря для загрузки</string>
<string name="cannot_query_associated_model" comment="Cannot reach server to check if there's an available dictionary to download"> Cannot connect to Keyman server to check for associated dictionary to download</string>

View file

@ -80,8 +80,6 @@
<string name="fileprovider_undefined" comment="FileProvider library needed to view help file"> Filleverantörsbibliotek behövs för att visa hjälpfilen: %1$s</string>
<!-- Context: Fatal keyboard error. Will load default keyboard -->
<string name="fatal_keyboard_error" comment="Fatal keyboard error (keyboard ID::packageID for language). Will load default keyboard"> Kritiskt tangentbordsfel med %1$s:%2$s för %3$s språk. Laddar standardtangentbordet.</string>
<!-- Context: Fatal keyboard error. -->
<string name="fatal_keyboard_error_short" comment="Error in keyboard (keyboard ID::packageID for language)."> Fel i tangentbordet %1$s:%2$s för %3$s språk.</string>
<!-- Context: Query for associated dictionary -->
<string name="query_associated_model" comment="Check if there's an available dictionary to download">Söker efter tillhörande ordbok att ladda ner</string>
<string name="cannot_query_associated_model" comment="Cannot reach server to check if there's an available dictionary to download"> Cannot connect to Keyman server to check for associated dictionary to download</string>

View file

@ -84,8 +84,6 @@
<string name="fileprovider_undefined" comment="FileProvider library needed to view help file"> Бібліотека FileProvider необхідна для перегляду файлу довідки: %1$s</string>
<!-- Context: Fatal keyboard error. Will load default keyboard -->
<string name="fatal_keyboard_error" comment="Fatal keyboard error (keyboard ID::packageID for language). Will load default keyboard"> Критична помилка в клавіатурі з %1$s:%2$s для мови %3$s . Завантаження клавіатури за замовчуванням.</string>
<!-- Context: Fatal keyboard error. -->
<string name="fatal_keyboard_error_short" comment="Error in keyboard (keyboard ID::packageID for language)."> Помилка в клавіатурі %1$s:%2$s для мови %3$s.</string>
<!-- Context: Query for associated dictionary -->
<string name="query_associated_model" comment="Check if there's an available dictionary to download">Перевірка пов\'язаного словника для завантаження</string>
<string name="cannot_query_associated_model" comment="Cannot reach server to check if there's an available dictionary to download"> Cannot connect to Keyman server to check for associated dictionary to download</string>

View file

@ -78,8 +78,6 @@
<string name="fileprovider_undefined" comment="FileProvider library needed to view help file">Cần có Thư viện FileProvider để xem phần trợ giúp file: %1$s</string>
<!-- Context: Fatal keyboard error. Will load default keyboard -->
<string name="fatal_keyboard_error" comment="Fatal keyboard error (keyboard ID::packageID for language). Will load default keyboard">Lỗi bàn phím nghiêm trọng với %1$s: %2$s cho ngôn ngữ %3$s. Đang tải bàn phím mặc định.</string>
<!-- Context: Fatal keyboard error. -->
<string name="fatal_keyboard_error_short" comment="Error in keyboard (keyboard ID::packageID for language).">Lỗi trong bàn phím %1$s:%2$s cho ngôn ngữ %3$s.</string>
<!-- Context: Alert Dialog body: System WebView not installed -->
<string name="body_install_webview" comment="Notification when WebView not installed"> Keyman yêu cầu phải cài đặt WebView\".</string>
<!-- Context: Alert Dialog body: System WebView installed but not enabled -->

View file

@ -78,8 +78,6 @@
<string name="fileprovider_undefined" comment="FileProvider library needed to view help file"> 需要文件提供者文库来查看帮助文件: %1$s</string>
<!-- Context: Fatal keyboard error. Will load default keyboard -->
<string name="fatal_keyboard_error" comment="Fatal keyboard error (keyboard ID::packageID for language). Will load default keyboard"> %1$s键盘错误:%2$s为%3$s 语言。正在加载默认键盘。</string>
<!-- Context: Fatal keyboard error. -->
<string name="fatal_keyboard_error_short" comment="Error in keyboard (keyboard ID::packageID for language)."> 对于 %3$s 语言,键盘出现错误 %1$s:%2$s 。</string>
<!-- Context: Query for associated dictionary -->
<string name="query_associated_model" comment="Check if there's an available dictionary to download">正在检查相关字典以下载</string>
<string name="cannot_query_associated_model" comment="Cannot reach server to check if there's an available dictionary to download"> 无法连接到 Keyman 服务器以检查要下载的相关字典</string>

View file

@ -123,13 +123,9 @@
<string name="fileprovider_undefined" comment="FileProvider library needed to view help file">
FileProvider library needed to view help file: %1$s</string>
<!-- Context: Fatal keyboard error. Will load default keyboard -->
<string name="fatal_keyboard_error" comment="Fatal keyboard error (keyboard ID::packageID for language). Will load default keyboard">
Fatal keyboard error with %1$s:%2$s for %3$s language. Loading default keyboard.</string>
<!-- Context: Fatal keyboard error. -->
<string name="fatal_keyboard_error_short" comment="Error in keyboard (keyboard ID::packageID for language).">
Error in keyboard %1$s:%2$s for %3$s language.</string>
<string name="fatal_keyboard_error" comment="Displays as a toast. Generally caused by a script error in keyboard view context, may be caused by keyboard or KeymanWeb.">
Keyman encountered an error and will restart</string>
<!-- Context: Alert Dialog body: System WebView not installed -->
<string name="body_install_webview" comment="Notification when WebView not installed">

View file

@ -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()) {
@ -111,6 +119,11 @@ public class KMManagerTest {
*/
@Test
public void create_newKeyboardsList() {
if(File.separatorChar != '/') {
Log.w(TAG, "create_newKeyboardsList skipped due to #16183");
return;
}
loadOldKeyboardsList();
dat_list = new ArrayList<HashMap<String, String>>();
@ -182,6 +195,12 @@ public class KMManagerTest {
@Test
public void test_updateOldKeyboardsList() {
// #16183
if(File.separatorChar != '/') {
Log.w(TAG, "test_updateOldKeyboardsList skipped due to #16183");
return;
}
loadOldKeyboardsList();
Assert.assertNotNull(dat_list);

View file

@ -23,7 +23,7 @@ public class FileUtilsTest {
List<ShadowLog.LogItem> logs = ShadowLog.getLogs();
// The logs contain type 4, but we only care about type 6 for connection messages
Assert.assertEquals(2, logs.size());
Assert.assertEquals("Size did not match 2: " + logs.toString(), 2, logs.size());
Assert.assertEquals("Connection", logs.get(0).tag);
Assert.assertEquals("Initialization failed:\njava.net.MalformedURLException: no protocol: invalidURL", logs.get(0).msg);

View file

@ -26,37 +26,48 @@ public class WebViewUtilsTest {
@Test
public void test_ChromeEmpty_EngineWebViewVersionStatusDisabled() {
String chromeVersion = "";
Assert.assertEquals(WebViewUtils.getEngineWebViewVersionStatus(context, null, chromeVersion),
WebViewUtils.EngineWebViewVersionStatus.DISABLED);
Assert.assertEquals(WebViewUtils.EngineWebViewVersionStatus.DISABLED,
WebViewUtils.getEngineWebViewVersionStatus(context, null, chromeVersion));
}
@Test
public void test_Chrome36_EngineWebViewVersionStatusDisabled() {
double chromeVersionFloat = Float.parseFloat(WebViewUtils.KEYMAN_MIN_TARGET_VERSION_DEGRADED_ANDROID_CHROME) - 1.0;
String chromeVersion = String.valueOf(chromeVersionFloat);
Assert.assertEquals(WebViewUtils.getEngineWebViewVersionStatus(context, null, chromeVersion),
WebViewUtils.EngineWebViewVersionStatus.DISABLED);
Assert.assertEquals(WebViewUtils.EngineWebViewVersionStatus.DISABLED,
WebViewUtils.getEngineWebViewVersionStatus(context, null, chromeVersion));
}
@Test
public void test_Chrome37_EngineWebViewVersionStatusDegraded() {
String chromeVersion = WebViewUtils.KEYMAN_MIN_TARGET_VERSION_DEGRADED_ANDROID_CHROME;
Assert.assertEquals(WebViewUtils.getEngineWebViewVersionStatus(context, null, chromeVersion),
WebViewUtils.EngineWebViewVersionStatus.DEGRADED);
Assert.assertEquals(WebViewUtils.EngineWebViewVersionStatus.DEGRADED,
WebViewUtils.getEngineWebViewVersionStatus(context, null, chromeVersion));
}
@Test
public void test_Chrome94_EngineWebViewVersionStatusDegraded() {
double chromeVersionFloat = Float.parseFloat(WebViewUtils.KEYMAN_MIN_TARGET_VERSION_ANDROID_CHROME) - 1.0;
String chromeVersion = String.valueOf(chromeVersionFloat);
Assert.assertEquals(WebViewUtils.getEngineWebViewVersionStatus(context, null, chromeVersion),
WebViewUtils.EngineWebViewVersionStatus.DEGRADED);
Assert.assertEquals(WebViewUtils.EngineWebViewVersionStatus.DEGRADED,
WebViewUtils.getEngineWebViewVersionStatus(context, null, chromeVersion));
}
@Test
public void test_Chrome95_EngineWebViewVersionStatusFull() {
String chromeVersion = WebViewUtils.KEYMAN_MIN_TARGET_VERSION_ANDROID_CHROME;
Assert.assertEquals(WebViewUtils.getEngineWebViewVersionStatus(context, null, chromeVersion),
WebViewUtils.EngineWebViewVersionStatus.FULL);
Assert.assertEquals(WebViewUtils.EngineWebViewVersionStatus.FULL,
WebViewUtils.getEngineWebViewVersionStatus(context, null, chromeVersion));
}
@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"));
}
}

View file

@ -1,4 +1,6 @@
#!/usr/bin/env bash
# Keyman is copyright (C) SIL Global. MIT License.
#
# Build Keyman Engine for Android using Keyman Web artifacts
#
## START STANDARD BUILD SCRIPT INCLUDE
@ -7,20 +9,9 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
. "${THIS_SCRIPT%/*}/../../resources/build/builder-full.inc.sh"
## END STANDARD BUILD SCRIPT INCLUDE
. "$KEYMAN_ROOT/resources/build/utils.inc.sh"
# ################################ Main script ################################
# Definition of global compile constants
KEYMAN_ANDROID_ROOT="$KEYMAN_ROOT/android"
KEYMAN_WEB_ROOT="$KEYMAN_ROOT/web"
ENGINE_ASSETS="$KEYMAN_ANDROID_ROOT/KMEA/app/src/main/assets"
CONFIG="release"
BUILD_FLAGS="aR -x lint -x test" # Gradle build w/o test
TEST_FLAGS="-x aR lintRelease testRelease" # Gradle test w/o build
JUNIT_RESULTS="##teamcity[importData type='junit' path='keyman\android\KMEA\app\build\test-results\testReleaseUnitTest\']"
builder_describe "Builds Keyman Engine for Android." \
"@/web/src/app/webview" \
"@/common/web/sentry-manager" \
@ -30,78 +21,64 @@ builder_describe "Builds Keyman Engine for Android." \
"test Runs lint and unit tests." \
":engine Builds Engine"
# parse before describe_outputs to check debug flags
builder_parse "$@"
# Definition of global compile constants
if builder_is_debug_build; then
builder_heading "### Debug config ####"
CONFIG="debug"
BUILD_FLAGS="assembleDebug -x lintDebug -x testDebug"
TEST_FLAGS="-x assembleDebug lintDebug testDebug"
JUNIT_RESULTS="##teamcity[importData type='junit' path='keyman\android\KMEA\app\build\test-results\testDebugUnitTest\']"
else
BUILD_FLAGS="assembleRelease -x lint -x test" # Gradle build w/o test
TEST_FLAGS="-x assembleRelease lintRelease testRelease" # Gradle test w/o build
JUNIT_RESULTS="##teamcity[importData type='junit' path='keyman\android\KMEA\app\build\test-results\testReleaseUnitTest\']"
fi
if builder_is_ci_build; then
GRADLE_DAEMON=--no-daemon
else
GRADLE_DAEMON=
fi
builder_describe_outputs \
build:engine /android/KMEA/app/build/outputs/aar/keyman-engine-${CONFIG}.aar
#### Build
# Parse args
DAEMON_FLAG=
if builder_is_ci_build; then
DAEMON_FLAG=--no-daemon
fi
build:engine /android/KMEA/app/build/outputs/aar/keyman-engine-${BUILDER_CONFIGURATION}.aar
#### Build action definitions ####
# Check about cleaning artifact paths
if builder_start_action clean:engine; then
rm -rf "$KEYMAN_ROOT/android/KMEA/app/build/outputs"
builder_finish_action success clean:engine
fi
if builder_start_action configure:engine; then
builder_finish_action success configure:engine
fi
# Destinations that will need the keymanweb artifacts
if builder_start_action build:engine; then
do_build() {
local ENGINE_ASSETS="$KEYMAN_ROOT/android/KMEA/app/src/main/assets"
# Copy KeymanWeb artifacts
echo "Copying Keyman Web artifacts"
cp "$KEYMAN_WEB_ROOT/build/app/webview/$CONFIG/keymanweb-webview.js" "$ENGINE_ASSETS/keymanweb-webview.js"
cp "$KEYMAN_WEB_ROOT/build/app/webview/$CONFIG/keymanweb-webview.js.map" "$ENGINE_ASSETS/keymanweb-webview.js.map"
cp "$KEYMAN_WEB_ROOT/build/app/webview/$CONFIG/map-polyfill.js" "$ENGINE_ASSETS/map-polyfill.js"
cp "$KEYMAN_WEB_ROOT/build/app/resources/osk/ajax-loader.gif" "$ENGINE_ASSETS/ajax-loader.gif"
cp "$KEYMAN_WEB_ROOT/build/app/resources/osk/kmwosk.css" "$ENGINE_ASSETS/kmwosk.css"
cp "$KEYMAN_WEB_ROOT/build/app/resources/osk/globe-hint.css" "$ENGINE_ASSETS/globe-hint.css"
cp "$KEYMAN_WEB_ROOT/build/app/resources/osk/keymanweb-osk.ttf" "$ENGINE_ASSETS/keymanweb-osk.ttf"
builder_echo "Copying Keyman Web artifacts"
cp "$KEYMAN_ROOT/web/build/app/webview/$BUILDER_CONFIGURATION/keymanweb-webview.js" "$ENGINE_ASSETS/"
cp "$KEYMAN_ROOT/web/build/app/webview/$BUILDER_CONFIGURATION/keymanweb-webview.js.map" "$ENGINE_ASSETS/"
cp "$KEYMAN_ROOT/web/build/app/webview/$BUILDER_CONFIGURATION/map-polyfill.js" "$ENGINE_ASSETS/"
cp "$KEYMAN_ROOT/web/build/app/resources/osk/ajax-loader.gif" "$ENGINE_ASSETS/"
cp "$KEYMAN_ROOT/web/build/app/resources/osk/kmwosk.css" "$ENGINE_ASSETS/"
cp "$KEYMAN_ROOT/web/build/app/resources/osk/globe-hint.css" "$ENGINE_ASSETS/"
cp "$KEYMAN_ROOT/web/build/app/resources/osk/keymanweb-osk.ttf" "$ENGINE_ASSETS/"
cp "$KEYMAN_ROOT/common/web/sentry-manager/build/lib/index.js" "$ENGINE_ASSETS/keyman-sentry.js"
echo "Copying es6-shim polyfill"
cp "$KEYMAN_ROOT/node_modules/es6-shim/es6-shim.min.js" "$ENGINE_ASSETS/es6-shim.min.js"
builder_echo "Copying es6-shim polyfill"
cp "$KEYMAN_ROOT/node_modules/es6-shim/es6-shim.min.js" "$ENGINE_ASSETS/"
builder_echo "BUILD_FLAGS $BUILD_FLAGS"
echo "BUILD_FLAGS $BUILD_FLAGS"
# Build without test
./gradlew $DAEMON_FLAG clean $BUILD_FLAGS
builder_finish_action success build:engine
fi
if builder_start_action test:engine; then
./gradlew $GRADLE_DAEMON $BUILD_FLAGS
}
do_test() {
if builder_is_ci_build; then
# Report JUnit test results to CI
echo "$JUNIT_RESULTS"
fi
echo "TEST_FLAGS: $TEST_FLAGS"
./gradlew $DAEMON_FLAG $TEST_FLAGS
builder_echo "TEST_FLAGS: $TEST_FLAGS"
./gradlew $GRADLE_DAEMON $TEST_FLAGS
}
builder_finish_action success test:engine
fi
builder_run_action clean:engine rm -rf build app/build
builder_run_action build:engine do_build
builder_run_action test:engine do_test

View file

@ -2,6 +2,9 @@ plugins {
id 'com.android.application'
}
ext.rootPath = '../../../'
apply from: "$rootPath/version.gradle"
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
@ -17,13 +20,22 @@ android {
noCompress "kmp"
}
buildFeatures {
buildConfig = true
}
defaultConfig {
applicationId "com.keyman.kmsample1"
minSdkVersion 21
targetSdkVersion 35
versionCode 1
versionName "1.0"
// KEYMAN_VERSION_CODE, KEYMAN_VERSION_NAME, KEYMAN_VERSION_FOR_FILENAME from version.gradle
versionCode KEYMAN_VERSION_CODE as Integer
versionName KEYMAN_VERSION_NAME
buildConfigField "String", "KEYMAN_VERSION_ENVIRONMENT", "\""+KEYMAN_VERSION_ENVIRONMENT+"\""
archivesBaseName = "KMSample1-$KEYMAN_VERSION_FOR_FILENAME"
}
buildTypes {
release {
minifyEnabled false
@ -40,7 +52,6 @@ android {
repositories {
google()
}
dependencies {
@ -50,4 +61,5 @@ dependencies {
implementation 'com.google.android.material:material:1.12.0'
implementation files('libs/keyman-engine.aar')
implementation 'androidx.preference:preference:1.2.1'
implementation 'androidx.webkit:webkit:1.14.0'
}

View file

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- allow Sentry reports -->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<application
android:allowBackup="true"

View file

@ -64,6 +64,7 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene
KMManager.KMDefault_KeyboardFont, // Font for KMSample1 text field
KMManager.KMDefault_KeyboardFont); // Font for OSK
KMManager.addKeyboard(this, kbInfo);
KMManager.setDefaultKeyboard(kbInfo); // Workaround for #16215
// Add a dictionary
HashMap<String, String>lexicalModelInfo = new HashMap<String, String>();
@ -132,7 +133,7 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
KMManager.onConfigurationChanged(newConfig);
resizeTextView(textView.isKeyboardVisible());
lastOrientation = newConfig.orientation;
}

View file

@ -3,7 +3,6 @@
buildscript {
repositories {
google()
jcenter()
mavenCentral()
}
dependencies {
@ -17,7 +16,6 @@ buildscript {
allprojects {
repositories {
google()
jcenter()
mavenCentral()
}
}

View file

@ -1,21 +1,16 @@
#!/usr/bin/env bash
# Keyman is copyright (C) SIL Global. MIT License.
#
# Samples: KMsample1
## START STANDARD BUILD SCRIPT INCLUDE
# adjust relative paths as necessary
THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
. "${THIS_SCRIPT%/*}/../../../resources/build/builder-full.inc.sh"
## END STANDARD BUILD SCRIPT INCLUDE
. "$KEYMAN_ROOT/resources/build/utils.inc.sh"
. "${KEYMAN_ROOT}/android/android-utils.inc.sh"
################################ Main script ################################
# Definition of global compile constants
CONFIG="release"
SAMPLE_FLAGS="build"
builder_describe "Build KMSample1 app for Android." \
"@/android/KMEA" \
"clean" \
@ -24,55 +19,19 @@ builder_describe "Build KMSample1 app for Android." \
"test" \
":app KMSample1"
# parse before describe_outputs to check debug flags
builder_parse "$@"
ARTIFACT="app-release-unsigned.apk"
if builder_is_debug_build; then
builder_heading "### Debug config ####"
CONFIG="debug"
SAMPLE_FLAGS="assembleDebug"
ARTIFACT="app-$CONFIG.apk"
fi
android_set_gradle_environment
builder_describe_outputs \
build:app /android/Samples/KMSample1/app/build/outputs/apk/$CONFIG/$ARTIFACT
# Parse args
if builder_is_ci_build; then
SAMPLE_FLAGS="$SAMPLE_FLAGS -no-daemon"
fi
build:app "/android/Samples/KMSample1/app/build/outputs/apk/${BUILDER_CONFIGURATION}/KMSample1-${KEYMAN_VERSION_FOR_FILENAME}-${ARCHIVE_TARGET}.apk"
#### Build action definitions ####
# Check about cleaning artifact paths
if builder_start_action clean:app; then
rm -rf "$KEYMAN_ROOT/android/Samples/KMSample1/app/build/outputs"
builder_finish_action success clean:app
fi
do_build() {
cp "$KEYMAN_ROOT/android/KMEA/app/build/outputs/aar/keyman-engine-${BUILDER_CONFIGURATION}.aar" "$KEYMAN_ROOT/android/Samples/KMSample1/app/libs/keyman-engine.aar"
./gradlew $GRADLE_DAEMON $GRADLE_TARGET
}
if builder_start_action configure:app; then
builder_finish_action success configure:app
fi
# Building KMSample1
if builder_start_action build:app; then
# Copy Keyman Engine for Android
cp "$KEYMAN_ROOT/android/KMEA/app/build/outputs/aar/keyman-engine-${CONFIG}.aar" "$KEYMAN_ROOT/android/Samples/KMSample1/app/libs/keyman-engine.aar"
./gradlew clean $SAMPLE_FLAGS
builder_finish_action success build:app
fi
if builder_start_action test:app; then
# TODO: define tests
builder_finish_action success test:app
fi
builder_run_action clean:app rm -rf build app/build
builder_run_action build:app do_build

View file

@ -2,6 +2,9 @@ plugins {
id 'com.android.application'
}
ext.rootPath = '../../../'
apply from: "$rootPath/version.gradle"
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
@ -17,13 +20,22 @@ android {
noCompress "kmp"
}
buildFeatures {
buildConfig = true
}
defaultConfig {
applicationId "com.keyman.kmsample2"
minSdkVersion 21
targetSdkVersion 35
versionCode 1
versionName "1.0"
// KEYMAN_VERSION_CODE, KEYMAN_VERSION_NAME, KEYMAN_VERSION_FOR_FILENAME from version.gradle
versionCode KEYMAN_VERSION_CODE as Integer
versionName KEYMAN_VERSION_NAME
buildConfigField "String", "KEYMAN_VERSION_ENVIRONMENT", "\""+KEYMAN_VERSION_ENVIRONMENT+"\""
archivesBaseName = "KMSample2-$KEYMAN_VERSION_FOR_FILENAME"
}
buildTypes {
release {
minifyEnabled false
@ -49,4 +61,5 @@ dependencies {
implementation 'com.google.android.material:material:1.12.0'
implementation files('libs/keyman-engine.aar')
implementation 'androidx.preference:preference:1.2.1'
implementation 'androidx.webkit:webkit:1.14.0'
}

View file

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- allow Sentry reports -->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<application
android:allowBackup="true"

View file

@ -57,6 +57,7 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven
KMManager.KMDefault_KeyboardFont, // Font for KMSample2
KMManager.KMDefault_KeyboardFont); // Font for OSK
KMManager.addKeyboard(this, kbInfo);
KMManager.setDefaultKeyboard(kbInfo); // Workaround for #16215
// Add a dictionary
HashMap<String, String>lexicalModelInfo = new HashMap<String, String>();
@ -262,4 +263,4 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven
public boolean onKeyLongPress(int keyCode, KeyEvent event) {
return interpreter.onKeyLongPress(keyCode, event);
}
}
}

View file

@ -3,7 +3,6 @@
buildscript {
repositories {
google()
jcenter()
mavenCentral()
}
dependencies {
@ -17,7 +16,6 @@ buildscript {
allprojects {
repositories {
google()
jcenter()
mavenCentral()
}
}

View file

@ -1,21 +1,16 @@
#!/usr/bin/env bash
# Keyman is copyright (C) SIL Global. MIT License.
#
# Samples: KMSample2
## START STANDARD BUILD SCRIPT INCLUDE
# adjust relative paths as necessary
THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
. "${THIS_SCRIPT%/*}/../../../resources/build/builder-full.inc.sh"
## END STANDARD BUILD SCRIPT INCLUDE
. "$KEYMAN_ROOT/resources/build/utils.inc.sh"
. "${KEYMAN_ROOT}/android/android-utils.inc.sh"
################################ Main script ################################
# Definition of global compile constants
CONFIG="release"
SAMPLE_FLAGS="build"
builder_describe "Build KMSample2 app for Android." \
"@/android/KMEA" \
"clean" \
@ -24,55 +19,19 @@ builder_describe "Build KMSample2 app for Android." \
"test" \
":app KMSample2"
# parse before describe_outputs to check debug flags
builder_parse "$@"
ARTIFACT="app-release-unsigned.apk"
if builder_is_debug_build; then
builder_heading "### Debug config ####"
CONFIG="debug"
SAMPLE_FLAGS="assembleDebug"
ARTIFACT="app-$CONFIG.apk"
fi
android_set_gradle_environment
builder_describe_outputs \
build:app /android/Samples/KMSample2/app/build/outputs/apk/$CONFIG/$ARTIFACT
# Parse args
if builder_is_ci_build; then
SAMPLE_FLAGS="$SAMPLE_FLAGS -no-daemon"
fi
build:app "/android/Samples/KMSample2/app/build/outputs/apk/${BUILDER_CONFIGURATION}/KMSample2-${KEYMAN_VERSION_FOR_FILENAME}-${ARCHIVE_TARGET}.apk"
#### Build action definitions ####
# Check about cleaning artifact paths
if builder_start_action clean:app; then
rm -rf "$KEYMAN_ROOT/android/Samples/KMSample2/app/build/outputs"
builder_finish_action success clean:app
fi
do_build() {
cp "$KEYMAN_ROOT/android/KMEA/app/build/outputs/aar/keyman-engine-${BUILDER_CONFIGURATION}.aar" "$KEYMAN_ROOT/android/Samples/KMSample2/app/libs/keyman-engine.aar"
./gradlew $GRADLE_DAEMON $GRADLE_TARGET
}
if builder_start_action configure:app; then
builder_finish_action success configure:app
fi
# Building KMSample2
if builder_start_action build:app; then
# Copy Keyman Engine for Android
cp "$KEYMAN_ROOT/android/KMEA/app/build/outputs/aar/keyman-engine-${CONFIG}.aar" "$KEYMAN_ROOT/android/Samples/KMSample2/app/libs/keyman-engine.aar"
./gradlew clean $SAMPLE_FLAGS
builder_finish_action success build:app
fi
if builder_start_action test:app; then
# TODO: define tests
builder_finish_action success test:app
fi
builder_run_action clean:app rm -rf build app/build
builder_run_action build:app do_build

View file

@ -29,10 +29,11 @@ android {
minSdkVersion 21
targetSdkVersion 35
// KEYMAN_VERSION_CODE and KEYMAN_VERSION_NAME from version.gradle
// KEYMAN_VERSION_CODE, KEYMAN_VERSION_NAME, KEYMAN_VERSION_FOR_FILENAME from version.gradle
versionCode KEYMAN_VERSION_CODE as Integer
versionName KEYMAN_VERSION_NAME
buildConfigField "String", "KEYMAN_VERSION_ENVIRONMENT", "\""+KEYMAN_VERSION_ENVIRONMENT+"\""
archivesBaseName = "KeyboardHarness-$KEYMAN_VERSION_FOR_FILENAME"
}
buildTypes {
release {
@ -60,4 +61,5 @@ dependencies {
implementation 'com.google.android.material:material:1.12.0'
implementation files('libs/keyman-engine.aar')
implementation 'androidx.preference:preference:1.2.1'
implementation 'androidx.webkit:webkit:1.14.0'
}

View file

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- allow Sentry reports -->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<application
android:allowBackup="true"

View file

@ -58,6 +58,7 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene
KMManager.KMDefault_KeyboardFont,
KMManager.KMDefault_KeyboardFont);
KMManager.addKeyboard(this, chiralityKBInfo);
Keyboard.setDefaultKeyboard(chiralityKBInfo);
// Longpress test keyboard
Keyboard longpressKBbInfo = new Keyboard(

View file

@ -1,21 +1,16 @@
#!/usr/bin/env bash
# Keyman is copyright (C) SIL Global. MIT License.
#
# Build Test app: KeyboardHarness
## START STANDARD BUILD SCRIPT INCLUDE
# adjust relative paths as necessary
THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
. "${THIS_SCRIPT%/*}/../../../resources/build/builder-full.inc.sh"
## END STANDARD BUILD SCRIPT INCLUDE
. "$KEYMAN_ROOT/resources/build/utils.inc.sh"
. "${KEYMAN_ROOT}/android/android-utils.inc.sh"
################################ Main script ################################
# Definition of global compile constants
CONFIG="release"
BUILD_FLAGS="aR -x lint -x test" # Gradle build w/o test
TEST_FLAGS="-x aR lintRelease testRelease" # Gradle test w/o build
builder_describe "Build KeyboardHarness test app for Android." \
"@/android/KMEA" \
"clean" \
@ -24,66 +19,19 @@ builder_describe "Build KeyboardHarness test app for Android." \
"test" \
":app KeyboardHarness"
# parse before describe outputs to check debug flags
builder_parse "$@"
ARTIFACT="app-release-unsigned.apk"
if builder_is_debug_build; then
builder_heading "### Debug config ####"
CONFIG="debug"
BUILD_FLAGS="assembleDebug -x lintDebug -x testDebug"
TEST_FLAGS="-x assembleDebug lintDebug testDebug"
ARTIFACT="app-$CONFIG.apk"
fi
android_set_gradle_environment
builder_describe_outputs \
build:app /android/Tests/KeyboardHarness/app/build/outputs/apk/$CONFIG/${ARTIFACT}
#### Build
#
# Prevents 'clear' on exit of mingw64 bash shell
#
SHLVL=0
# Parse args
# Build flags that apply to all targets
if builder_is_ci_build; then
BUILD_FLAGS="$BUILD_FLAGS -no-daemon"
TEST_FLAGS="$TEST_FLAGS -no-daemon"
fi
build:app "/android/Tests/KeyboardHarness/app/build/outputs/apk/${BUILDER_CONFIGURATION}/KeyboardHarness-${KEYMAN_VERSION_FOR_FILENAME}-${ARCHIVE_TARGET}.apk"
#### Build action definitions ####
# Check about cleaning artifact paths
if builder_start_action clean:app; then
rm -rf "$KEYMAN_ROOT/android/Tests/KeyboardHarness/app/build/outputs"
builder_finish_action success clean:app
fi
do_build() {
cp "$KEYMAN_ROOT/android/KMEA/app/build/outputs/aar/keyman-engine-${BUILDER_CONFIGURATION}.aar" ./app/libs/keyman-engine.aar
./gradlew $GRADLE_DAEMON $GRADLE_TARGET
}
if builder_start_action configure:app; then
builder_finish_action success configure:app
fi
# Building KeyboardHarness
if builder_start_action build:app; then
# Copy Keyman Engine for Android
cp "$KEYMAN_ROOT/android/KMEA/app/build/outputs/aar/keyman-engine-${CONFIG}.aar" "$KEYMAN_ROOT/android/Tests/KeyboardHarness/app/libs/keyman-engine.aar"
echo "BUILD_FLAGS: $BUILD_FLAGS"
./gradlew clean $BUILD_FLAGS
builder_finish_action success build:app
fi
if builder_start_action test:app; then
echo "TEST_FLAGS $TEST_FLAGS"
# TODO: define tests
builder_finish_action success test:app
fi
builder_run_action clean:app rm -rf build app/build
builder_run_action build:app do_build

View file

@ -0,0 +1,20 @@
# shellcheck shell=bash
# Keyman is copyright (C) SIL Global. MIT License.
function android_set_gradle_environment() {
if builder_is_debug_build; then
GRADLE_TARGET=assembleDebug
ARCHIVE_TARGET=debug
else
GRADLE_TARGET=build
ARCHIVE_TARGET=release-unsigned
fi
if builder_is_ci_build; then
GRADLE_DAEMON=-no-daemon
else
GRADLE_DAEMON=
fi
export GRADLE_TARGET GRADLE_DAEMON ARCHIVE_TARGET
}

View file

@ -1,4 +1,5 @@
#!/usr/bin/env bash
# Keyman is copyright (C) SIL Global. MIT License.
#
# Build Keyman Engine for Android, Keyman for Android, OEM FirstVoices Android app,
# Samples: KMsample1 and KMSample2, Test - KeyboardHarness
@ -50,11 +51,6 @@ builder_describe \
builder_parse "$@"
function do_clean() {
builder_heading "Cleanup /android/upload"
rm -rf "${KEYMAN_ROOT}/android/upload"
}
function do_test_help() {
check-markdown "${KEYMAN_ROOT}/android/docs/help"
check-markdown "${KEYMAN_ROOT}/android/docs/engine"
@ -62,7 +58,6 @@ function do_test_help() {
function archive_artifacts() {
local UPLOAD_PATH KEYMAN_ENGINE_ANDROID_ZIP KEYMAN_APK FIRSTVOICES_APK
local KEYMAN_FULLY_VERSIONED_APK FIRSTVOICES_FULLY_VERSIONED_APK
UPLOAD_PATH="${KEYMAN_ROOT}/android/upload/${KEYMAN_VERSION}"
KEYMAN_ENGINE_ANDROID_ZIP="keyman-engine-android-${KEYMAN_VERSION}.zip"
@ -124,12 +119,13 @@ android_set_java_home
# This script also responsible for cleaning up /android/upload
builder_run_child_actions clean
builder_run_action clean do_clean
builder_run_action clean rm -rf "${KEYMAN_ROOT}/android/upload"
builder_run_child_actions configure build test
builder_run_action test:help do_test_help
builder_run_action test:help do_test_help
# TODO: merge publish-* and archive to publish action?
builder_run_child_actions publish-symbols publish-play-store
builder_run_action archive archive_artifacts
builder_run_action archive archive_artifacts

View file

@ -32,39 +32,6 @@ can be selected from the keyboards menu. If the keyboard with same
keyboard ID and language ID exists, it updates the existing keyboard
info.
------------------------------------------------------------------------
## Syntax (Deprecated)
``` javascript
KMManager.addKeyboard(Context context, HashMap<String, String> keyboardInfo)
```
### Parameters
`context`
: The context.
`keyboardInfo`
: A dictionary of keyboard information with keys and values defined as
`HashMap<String key, String value>`.
### Returns
Returns `true` if the keyboard was added successfully, `false`
otherwise.
## Description
Use this method to include a keyboard in the keyboards list so that it
can be selected from the keyboards menu. If the keyboard with same
keyboard ID and language ID exists, it updates the existing keyboard
info.
------------------------------------------------------------------------
## Examples

View file

@ -28,34 +28,6 @@ Use this method to get details of the currently selected keyboard.
Details include package ID, keyboard ID, language ID, keyboard name,
language name and fonts.
------------------------------------------------------------------------
## Syntax (Deprecated)
``` javascript
KMManager.getCurrentKeyboardInfo(Context context)
```
### Parameters
`context`
: The context.
### Returns
(Deprecated) Returns an information dictionary of the current keyboard
with keys and values defined as `HashMap<String key, String value>`.
## Description
Use this method to get details of the currently selected keyboard.
Details include keyboard ID, language ID, keyboard name, language name
and fonts.
------------------------------------------------------------------------
## Examples

View file

@ -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");
String textFontFilename = KMManager.getKeyboardTextFontFilename();
Typeface fontTypeface = KMManager.getFontTypeface(this, textFontFilename);
textView.setTypeface(fontTypeface);
```

View file

@ -22,6 +22,8 @@ empty string otherwise.
Use this method to get the font filename of the selected keyboard.
Deprecated. Use `getKeyboardTextFontFilename()` instead.
## Examples
### Example: Using `getKeyboardFontFilename()`
@ -34,5 +36,6 @@ The following script illustrate the use of `getKeyboardFontFilename()`:
## See also
- [`getKeyboardTextFontFilename()`](getKeyboardTextFontFilename)
- [`getKeyboardFontTypeface()` (Deprecated)](getKeyboardFontTypeface)
- [`getFontTypeface()`](getFontTypeface)

View file

@ -28,6 +28,8 @@ exists, `null` otherwise.
Use this method to create a new typeface from the selected keyboard's
font if it has any.
Deprecated. Use `getKeyboardTextFontFilename()` instead.
## Examples
### Example: Using `getKeyboardFontTypeface()`
@ -44,3 +46,4 @@ The following script illustrate the use of `getKeyboardFontTypeface()`:
- [`getFontTypeface()`](getFontTypeface)
- [`getKeyboardFontFilename()` (Deprecated)](getKeyboardFontFilename)
- [`getKeyboardTextFontFilename()`](getKeyboardTextFontFilename)

View file

@ -31,35 +31,6 @@ Use this method to get details of the keyboard at given position in
keyboards list. Details include keyboard ID, language ID, keyboard name,
language name and fonts.
------------------------------------------------------------------------
## Syntax (Deprecated)
``` javascript
KMManager.getKeyboardInfo(Context context, int index)
```
### Parameters
`context`
: The context.
`index`
: 0-based position of the keyboard in keyboards list.
### Returns
(Deprecated) Returns an information dictionary of the specified keyboard
with keys and values defined as `HashMap<String key, String value>`.
## Description
Use this method to get details of the keyboard at given position in
keyboards list. Details include keyboard ID, language ID, keyboard name,
language name and fonts.
## Examples
### Example: Using `getKeyboardInfo()`

View file

@ -5,18 +5,25 @@ 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. 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
@ -29,8 +36,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

View file

@ -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

View file

@ -26,33 +26,6 @@ otherwise.
Use this method to get details of all keyboard's in keyboards menu.
------------------------------------------------------------------------
## Syntax (Deprecated)
``` javascript
KMManager.getKeyboardsList(Context context)
```
### Parameters
`context`
: The context.
### Returns
(Deprecated) Returns keyboards list as
`ArrayList<HashMap<String key, String value>>` if it exists, `null`
otherwise.
## Description
Use this method to get details of all keyboard's in keyboards menu.
------------------------------------------------------------------------
## Examples

View file

@ -35,8 +35,8 @@ normally returned by [`getKeyboardInfo()`](getKeyboardInfo).
```java
KMManager.setKeyboard(String packageID, String keyboardID, String languageID)
KMManager.setKeyboard(String packageID, String keyboardID, String languageID, String keyboardName, String languageName, String kFont, String kOskFont)
KMManager.setKeyboard(String packageID, String keyboardID, String languageID, String keyboardName, String languageName, String kFont, String kOskFont, String displayName)
KMManager.setKeyboard(String packageID, String keyboardID, String languageID, String keyboardName, String languageName, String textFontFilename, String oskFontFilename)
KMManager.setKeyboard(String packageID, String keyboardID, String languageID, String keyboardName, String languageName, String textFontFilename, String oskFontFilename, String displayName)
```
### Parameters
@ -56,13 +56,13 @@ KMManager.setKeyboard(String packageID, String keyboardID, String languageID, St
`languageName`
: Name of the associated language.
`kFont`
: Filename or description of the font to type with the keyboard. Can be `null`
`textFontFilename`
: Filename of the font to type with the keyboard. Can be `null`
or empty string.
`kOskFont`
: Filename or description of the font displayed on the keyboard. Can be `null`
or empty string.
`oskFontFilename`
: Filename of the font displayed on the keyboard. Can be `null`
or empty string, and if `null` or empty string, uses `textFontFilename`.
`displayName`
: A text string to display on the spacebar identifying this keyboard; if `null`,

View file

@ -88,6 +88,16 @@ def getVersionGitTag = { ->
return "release@$KEYMAN_VERSION_NAME"
}
}
def getVersionForFilename = { ->
String env_version_for_filename = System.getenv("KEYMAN_VERSION_FOR_FILENAME")
if(env_version_for_filename != null) {
return env_version_for_filename
}
// Building probably from IDE
return "$KEYMAN_VERSION_NAME"
}
ext {
KEYMAN_VERSION_CODE=getVersionCode()
KEYMAN_VERSION_MD=getVersionMD()
@ -95,6 +105,7 @@ ext {
KEYMAN_VERSION_ENVIRONMENT=getVersionEnvironment()
KEYMAN_VERSION_TAG=System.getenv("KEYMAN_VERSION_TAG")
KEYMAN_VERSION_GIT_TAG=getVersionGitTag()
KEYMAN_VERSION_FOR_FILENAME=getVersionForFilename()
}
//println "version.gradle: KEYMAN_VERSION_TAG: " + KEYMAN_VERSION_TAG

Some files were not shown because too many files have changed in this diff Show more