Merge pull request #16324 from keymanapp/auto/A19S34-merge-base-into-kmc-convert
Some checks failed
Keyman Build Summary / Summarize build status checks (push) Has been cancelled

auto: A19S34 merge master into kmc-convert 😎
This commit is contained in:
Keyman Server 2026-08-10 12:41:07 +02:00 • committed by GitHub
commit d3b07e5768
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 520 additions and 143 deletions

View file

@ -1,5 +1,24 @@
# Keyman Version History
## 19.0.262 alpha 2026-08-04
* chore(web): fire `keyboardloaded` event only once per keyboard (#16297)
## 19.0.261 alpha 2026-07-30
* fix(core): clone state actions deep copy (#15961)
* maint(android): update Android Target API to 36 (#16280)
* refactor(web): adjust function visibility and variable names (#16286)
## 19.0.260 alpha 2026-07-27
* fix(web): ignore exception trying to access cookie (#16283)
* chore(web): update help links to use latest version (#16285)
## 19.0.259 alpha 2026-07-20
* docs(web): fix typos following #16170 (#16225)
## 19.0.258 alpha 2026-07-15
* chore(android): remove doc for non-existing deprecated functions (#16244)

View file

@ -1 +1 @@
19.0.259
19.0.263

View file

@ -17,7 +17,7 @@ java {
}
android {
compileSdk 35
compileSdk 36
namespace="com.tavultesoft.kmapro"
// Don't compress kmp files so they can be copied via AssetManager
@ -32,7 +32,7 @@ android {
defaultConfig {
applicationId "com.tavultesoft.kmapro"
minSdkVersion 21
targetSdkVersion 35
targetSdkVersion 36
//println "===DUMPING PROPERTIES==="
//dumpProperties(project) // Use this to dump all external properties for debugging TeamCity integration

View file

@ -468,7 +468,7 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene
return true;
} else if (item.getItemId() == R.id.action_update_keyboards) {
KMManager.getUpdateTool().executeOpenUpdates();
// Dismiss icon
// Dismiss icon - we intentionally hide the icon, even if the user cancels the update
updateUpdateCountIndicator(0);
final MenuItem _keyboardupdate = menu.findItem(R.id.action_update_keyboards);
if (_keyboardupdate != null && _keyboardupdate.isVisible()) {

View file

@ -19,7 +19,7 @@ java {
}
android {
compileSdk 35
compileSdk 36
namespace "com.keyman.engine"
buildFeatures {
@ -29,7 +29,7 @@ android {
defaultConfig {
minSdkVersion 21
targetSdkVersion 35
targetSdkVersion 36
// KEYMAN_VERSION_CODE and KEYMAN_VERSION_NAME from version.gradle but Gradle removes them for libraries
buildConfigField "String", "KEYMAN_ENGINE_VERSION_NAME", "\""+KEYMAN_VERSION_NAME+"\""

View file

@ -263,14 +263,14 @@ public final class KeyboardPickerActivity extends BaseActivity {
// Determine the index to the current keyboard position to highlight as the selected keyboard
int currentKeyboardIndex;
int currentKeyboardIndex;
String currentKeyboardKey = KMKeyboard.currentKeyboard();
if (currentKeyboardKey == null) {
SharedPreferences prefs = this.getSharedPreferences(this.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE);
currentKeyboardIndex = prefs.getInt(KMManager.KMKey_UserKeyboardIndex, 0);
} else {
currentKeyboardIndex = KeyboardController.getInstance().getKeyboardIndex(currentKeyboardKey);
}
}
setSelection(currentKeyboardIndex);
imeList = getIMEList(this);
@ -756,6 +756,32 @@ public final class KeyboardPickerActivity extends BaseActivity {
return index;
}
/**
* Get an array list with all lexical model hashmaps matching the lexical model ID.
* @param context
* @param lexicalModelId - ID of the lexical model
* @return Array of lexical model hashmaps with lexicalModelId. Otherwise empty.
*/
public static ArrayList<HashMap<String, String>> getLexicalModelsMatchingId(Context context, String lexicalModelId) {
ArrayList<HashMap<String, String>> lexicalModels = new ArrayList<>();
if (lexicalModelsList == null) {
lexicalModelsList = getLexicalModelsList(context);
}
if (lexicalModelsList != null) {
int length = lexicalModelsList.size();
for (int i=0; i < length; i++) {
HashMap<String, String> lmInfo = lexicalModelsList.get(i);
if (lexicalModelId.equals(lmInfo.get(KMManager.KMKey_LexicalModelID))) {
lexicalModels.add(lmInfo);
}
}
}
return lexicalModels;
}
public static HashMap<String, String> getLexicalModelInfo(Context context,int index) {
if (index < 0) {
return null;

View file

@ -11,6 +11,7 @@ import com.keyman.engine.KMManager;
import com.keyman.engine.KeyboardPickerActivity;
import com.keyman.engine.R;
import com.keyman.engine.cloud.CloudApiTypes;
import com.keyman.engine.data.CloudRepository;
import com.keyman.engine.data.Keyboard;
import com.keyman.engine.data.KeyboardController;
import com.keyman.engine.data.LexicalModel;
@ -188,7 +189,6 @@ public class CloudDataJsonUtil {
}
public static void processLexicalModelPackageUpdateJSON(Context aContext, JSONObject pkgData, List<Bundle> updateBundles) {
boolean saveModelsList = false;
// Parse for lexical model package updates
if (pkgData.has(CDKey_Models)) {
try {
@ -201,18 +201,11 @@ public class CloudDataJsonUtil {
String cloudVersion = cloudModelObj.getString(CDKey_Version);
String cloudKMP = cloudModelObj.getString(CDKey_KMP);
// Valid lexical model package exists. See if lexical model list needs to be updated
// Valid keyboard package exists. See if keyboard list needs to be updated
int index = KeyboardPickerActivity.getLexicalModelIndex(aContext, lexicalModelID);
if (index != -1) {
HashMap<String, String> lmInfo = KeyboardPickerActivity.getLexicalModelInfo(aContext, index);
String version = lmInfo.get(KMManager.KMKey_Version);
ArrayList<HashMap<String, String>> lmInfos = KeyboardPickerActivity.getLexicalModelsMatchingId(aContext, lexicalModelID);
for (HashMap<String, String> lmInfo : lmInfos) {
String version = lmInfo.get(KMManager.KMKey_LexicalModelVersion);
if (lexicalModelID.equalsIgnoreCase(lmInfo.get(KMManager.KMKey_LexicalModelID)) &&
(FileUtils.compareVersions(cloudVersion, version) == FileUtils.VERSION_GREATER) &&
(!MapCompat.getOrDefault(lmInfo, KMManager.KMKey_KMPLink, "").equalsIgnoreCase(cloudKMP))) {
// Update keyboard with the latest KMP link
lmInfo.put(KMManager.KMKey_KMPLink, cloudKMP);
KeyboardPickerActivity.addLexicalModel(aContext, lmInfo);
(FileUtils.compareVersions(cloudVersion, version) == FileUtils.VERSION_GREATER)) {
// Update bundle list
LexicalModel lm = new LexicalModel(
lmInfo.get(KMManager.KMKey_PackageID),
@ -220,13 +213,25 @@ public class CloudDataJsonUtil {
lmInfo.get(KMManager.KMKey_LexicalModelName),
lmInfo.get(KMManager.KMKey_LanguageID),
lmInfo.get(KMManager.KMKey_LanguageName),
lmInfo.get(KMManager.KMKey_Version),
lmInfo.get(KMManager.KMKey_LexicalModelVersion),
lmInfo.get(KMManager.KMKey_CustomHelpLink),
lmInfo.get(KMManager.KMKey_KMPLink));
Bundle bundle = new Bundle(lm.buildDownloadBundle());
updateBundles.add(bundle);
String updateKMP = lm.getUpdateKMP();
if (cloudLinkIsNewer(updateKMP, cloudKMP)) {
// Update lexical model info with the latest KMP link after appending languageID
String languageID = lm.getLanguageID();
String link = String.format("%s&bcp47=%s", cloudKMP, languageID);
lmInfo.put(KMManager.KMKey_KMPLink, link);
KeyboardPickerActivity.addLexicalModel(aContext, lmInfo);
lm.setUpdateKMP(link);
updateKMP = link;
}
saveModelsList = true;
if (updateKMP != null && !updateKMP.isEmpty()) {
// Update bundle list for update notifications
Bundle bundle = new Bundle(lm.buildDownloadBundle());
updateBundles.add(bundle);
}
}
}
}
@ -367,14 +372,13 @@ public class CloudDataJsonUtil {
return false;
}
if (updateKMP == null || updateKMP.isEmpty()) {
return true;
}
try {
Uri cloudLink = Uri.parse(cloudKMP);
if (updateKMP == null || updateKMP.isEmpty()) {
return true;
}
Uri localLink = Uri.parse(updateKMP);
Uri cloudLink = Uri.parse(cloudKMP);
boolean pathsMatch = localLink.getLastPathSegment().equalsIgnoreCase(cloudLink.getLastPathSegment());
boolean cloudVersionNewer = FileUtils.compareVersions(

View file

@ -154,6 +154,7 @@ public class CloudCatalogDownloadCallback implements ICloudDownloadCallback<Data
// The actual update check
CloudDataJsonUtil.processKeyboardPackageUpdateJSON(context, jsonTuple.packagesJSON, updateBundles);
CloudDataJsonUtil.processLexicalModelPackageUpdateJSON(context, jsonTuple.packagesJSON, updateBundles);
// Only add installed kmp keyboards
aDataSet.keyboards.clear();

View file

@ -21,6 +21,7 @@ import com.keyman.engine.cloud.CloudDownloadMgr;
import com.keyman.engine.packages.JSONUtils;
import com.keyman.engine.util.BCP47;
import com.keyman.engine.util.DownloadFileUtils;
import com.keyman.engine.util.FileUtils;
import com.keyman.engine.util.KMLog;
import com.keyman.engine.util.VersionUtils;
@ -153,8 +154,10 @@ public class CloudRepository {
/**
* Search the available lexical models list and see there's an associated model for a
* given language ID. Available models are from the cloud catalog and locally installed models.
* @param context Context
* @param languageID String of the language ID to search
* If more than one model are installed for a given language, the first one found will be
* returned.
* @param context Context
* @param languageID String of the language ID to search
* @return LexicalModel of an associated lexical model. Null if no match found
*/
public LexicalModel getAssociatedLexicalModel(@NonNull Context context, String languageID) {
@ -253,6 +256,49 @@ public class CloudRepository {
downloadMetaDataFromServer(context,updateHandler,onSuccess,onFailure);
}
/**
* Merges the new lexical models into the existing dataset models. If
* a model already exists, it compares the versions and uses the newer one.
* Otherwise the model gets added to the dataset. Models that exist
* in the dataset but not in the new models list are preserved.
* @param datasetModels Collection of lexical models in the dataset
* @param newModels List of new lexical models to merge
*/
private void mergeLexicalModels(Dataset.LexicalModels datasetModels, List<LexicalModel> newModels) {
if (newModels == null) {
return;
}
List<LexicalModel> existingModels = new ArrayList<>(datasetModels.asList());
List<LexicalModel> mergedModels = new ArrayList<>();
// Process all incoming models
for (LexicalModel newModel : newModels) {
LexicalModel existingMatch = null;
for (int i = 0; i < existingModels.size(); i++) {
if (newModel.equals(existingModels.get(i))) {
existingMatch = existingModels.remove(i);
break;
}
}
if (existingMatch != null) {
if (FileUtils.compareVersions(existingMatch.getVersion(), newModel.getVersion()) != FileUtils.VERSION_LOWER) {
mergedModels.add(existingMatch);
} else {
mergedModels.add(newModel);
}
} else {
mergedModels.add(newModel);
}
}
// Add remaining existing models that weren't matched
mergedModels.addAll(existingModels);
datasetModels.clear();
datasetModels.addAll(mergedModels);
}
/**
* precache dataset and notify callbacks if no update from cloud api services is necessary.
@ -286,11 +332,13 @@ public class CloudRepository {
languageCodes.add(installedSet.getItem(i).code);
}
// add all models that are installed locally
memCachedDataset.lexicalModels.addAll(installedSet.lexicalModels.asList());
// Get kmp.json info from installed (adhoc and cloud) models.
// Consolidate kmp.json info from packages/
JSONObject kmpLanguagesArray = wrapKmpKeyboardJSON(JSONUtils.getLanguages());
JSONArray kmpLexicalModelsArray = JSONUtils.getLexicalModels();
final boolean fromKMP = true;
try {
if (kmpLanguagesArray.getJSONObject(KMKeyboardDownloaderActivity.KMKey_Languages).
@ -298,7 +346,10 @@ public class CloudRepository {
memCachedDataset.keyboards.addAll(CloudDataJsonUtil.processKeyboardJSON(kmpLanguagesArray, true));
}
if (kmpLexicalModelsArray.length() > 0) {
memCachedDataset.lexicalModels.addAll(CloudDataJsonUtil.processLexicalModelJSON(kmpLexicalModelsArray, fromKMP));
// Add all models that exist locally, whether or not they are installed.
// This set of models doesn't have the download url set, so instead of
// replacing the models in `memCacheDataSet.lexicalModels` we merge them.
mergeLexicalModels(memCachedDataset.lexicalModels, CloudDataJsonUtil.processLexicalModelJSON(kmpLexicalModelsArray, true));
}
} catch (Exception e) {
KMLog.LogException(TAG, "preCacheDataSet error ", e);

View file

@ -0,0 +1,149 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*/
package com.keyman.engine.data;
import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
@RunWith(RobolectricTestRunner.class)
public class CloudRepositoryTests {
private CloudRepository repository;
private Dataset dataset;
private Context context;
private boolean containsModel(Dataset.LexicalModels models, String modelID) {
for (int i = 0; i < models.getCount(); i++) {
if (models.getItem(i).getLexicalModelID().equals(modelID)) {
return true;
}
}
return false;
}
private void setMemCachedDataset(Dataset dataset) throws Exception {
Field field = CloudRepository.class.getDeclaredField("memCachedDataset");
field.setAccessible(true);
field.set(repository, dataset);
}
@Before
public void setUp() {
context = ApplicationProvider.getApplicationContext();
repository = CloudRepository.shared;
dataset = new Dataset(context);
}
@After
public void tearDown() throws Exception {
// Reset singleton state
setMemCachedDataset(null);
}
@Test
public void testMergeLexicalModels_PreservesUniqueExistingModels() throws Exception {
// Setup
dataset.lexicalModels.add(new LexicalModel("pkg1", "model1", "Model 1", "en", "English", "1.0", "", ""));
List<LexicalModel> newModels = new ArrayList<>();
newModels.add(new LexicalModel("pkg2", "model2", "Model 2", "fr", "French", "1.0", "", ""));
// Execute
Method mergeMethod = CloudRepository.class.getDeclaredMethod("mergeLexicalModels", Dataset.LexicalModels.class, List.class);
mergeMethod.setAccessible(true);
mergeMethod.invoke(repository, dataset.lexicalModels, newModels);
// Verify
Assert.assertEquals(2, dataset.lexicalModels.getCount());
Assert.assertTrue(containsModel(dataset.lexicalModels, "model1"));
Assert.assertTrue(containsModel(dataset.lexicalModels, "model2"));
}
@Test
public void testMergeLexicalModels_UpdatesVersion() throws Exception {
// Setup
dataset.lexicalModels.add(new LexicalModel("pkg1", "model1", "Model 1", "en", "English", "1.0", "", ""));
List<LexicalModel> newModels = new ArrayList<>();
// New model (same ID, newer version v1.1)
newModels.add(new LexicalModel("pkg1", "model1", "Model 1", "en", "English", "1.1", "", ""));
// Execute
Method mergeMethod = CloudRepository.class.getDeclaredMethod("mergeLexicalModels", Dataset.LexicalModels.class, List.class);
mergeMethod.setAccessible(true);
mergeMethod.invoke(repository, dataset.lexicalModels, newModels);
// Verify
Assert.assertEquals(1, dataset.lexicalModels.getCount());
LexicalModel result = dataset.lexicalModels.getItem(0);
Assert.assertEquals("1.1", result.getVersion());
}
@Test
public void testMergeLexicalModels_KeepsOlderIfNewerIsLowerVersion() throws Exception {
// Setup
dataset.lexicalModels.add(new LexicalModel("pkg1", "model1", "Model 1", "en", "English", "1.2", "", ""));
List<LexicalModel> newModels = new ArrayList<>();
// New model (same ID, older version v1.1)
newModels.add(new LexicalModel("pkg1", "model1", "Model 1", "en", "English", "1.1", "", ""));
// Execute
Method mergeMethod = CloudRepository.class.getDeclaredMethod("mergeLexicalModels", Dataset.LexicalModels.class, List.class);
mergeMethod.setAccessible(true);
mergeMethod.invoke(repository, dataset.lexicalModels, newModels);
// Verify
Assert.assertEquals(1, dataset.lexicalModels.getCount());
LexicalModel result = dataset.lexicalModels.getItem(0);
Assert.assertEquals("1.2", result.getVersion());
}
@Test
public void testGetAssociatedLexicalModel_ReturnsCorrectModel() throws Exception {
// Setup
LexicalModel model1 = new LexicalModel("pkg1", "model1", "Model 1", "en", "English", "1.0", "", "");
LexicalModel model2 = new LexicalModel("pkg2", "model2", "Model 2", "fr", "French", "1.0", "", "");
dataset.lexicalModels.add(model1);
dataset.lexicalModels.add(model2);
setMemCachedDataset(dataset);
// Execute & Verify
Assert.assertEquals(model1, repository.getAssociatedLexicalModel(context, "en"));
Assert.assertEquals(model2, repository.getAssociatedLexicalModel(context, "fr"));
Assert.assertEquals(model1, repository.getAssociatedLexicalModel(context, "EN")); // Case insensitivity
}
@Test
public void testGetAssociatedLexicalModel_ReturnsNullWhenDatasetIsNull() throws Exception {
// Setup
setMemCachedDataset(null);
// Execute & Verify
Assert.assertNull(repository.getAssociatedLexicalModel(context, "en"));
}
@Test
public void testGetAssociatedLexicalModel_ReturnsFirstMatch() throws Exception {
// Setup
LexicalModel model1 = new LexicalModel("pkg1", "model1", "Model 1", "en", "English", "1.0", "", "");
LexicalModel model2 = new LexicalModel("pkg2", "model2", "Model 2", "en", "English", "1.0", "", "");
dataset.lexicalModels.add(model1);
dataset.lexicalModels.add(model2);
setMemCachedDataset(dataset);
// Execute & Verify
Assert.assertEquals(model1, repository.getAssociatedLexicalModel(context, "en"));
}
}

View file

@ -79,8 +79,10 @@ analytics for Debug are associated with an App Bundle ID
Replace `SERIAL` with the device serial number listed in step 2.
### Compiling the app's offline help
Keyman for Android help is maintained in the Markdown files in android/docs/help.
The script `/resources/build/build-help.inc.sh` uses the `pandoc` tool to convert the Markdown files into html.
The script `/resources/build/build-help.inc.sh` uses the `pandoc` tool to convert
the Markdown files into html.
```bash
# Convert markdown to html for offline help
@ -95,7 +97,9 @@ There are two included sample projects that can be modified to test a keyboard.
**android/Samples/KMSample1** app runs a bare Keyman app for testing a keyboard.
**android/Samples/KMSample2** app provides prompts for setting KMSample2 as a system level keyboard.
**android/Samples/KMSample2** app provides prompts for setting KMSample2 as a
system level keyboard.
Both sample apps include a default Tamil keyboard and sample dictionary.
Building the debug versions of these projects follow the same steps as KMAPro:
@ -106,12 +110,16 @@ Building the debug versions of these projects follow the same steps as KMAPro:
### Tests: KeyboardHarness
**android/Tests/KeyboardHarness** app is a test harness for developers to troubleshoot keyboards.
**android/Tests/KeyboardHarness** app is a test harness for developers to
troubleshoot keyboards.
1. Copy the keyboard js file and applicable ttf fonts to *android/Tests/KeyboardHarness/app/src/main/assets/*.
2. In Keyman Developer
* Open the `keyboardharness.kpj` project and add your keyboard files to the keyboard package.
* Build the keyboardharness.kmp keyboard package
* Open the `keyboardharness.kpj` project and add your keyboard files to the
keyboard package.
* Build the keyboardharness.kmp keyboard package
3. Add the keyboard in *android/Tests/KeyboardHarness/app/src/main/java/com/keyman/android/tests/keyboardHarness/MainActivity.java*
4. cd to android/Tests/KeyboardHarness/
5. `./build.sh configure build`
@ -120,62 +128,74 @@ Building the debug versions of these projects follow the same steps as KMAPro:
--------------------------------------------------------------
## How to Build Keyman Engine for Android
1. Open a terminal or Git Bash prompt and go to the Android project folder (e.g. `cd ~/keyman/android/`)
1. Open a terminal or Git Bash prompt and go to the Android project folder
(e.g. `cd ~/keyman/android/`)
2. Run `./build.sh build:engine`
Keyman Engine for Android library (**keyman-engine.aar**) is now ready to be imported in any project.
Keyman Engine for Android library (**keyman-engine.aar**) is now ready to be
imported in any project.
## How to Use Keyman Engine for Android Library
1. Add **keyman-engine.aar** into **[Your project folder]/app/libs/** folder.
a. We recommend [downloading](https://keyman.com/downloads/#android-engine) the the latest stable release of Keyman Engine and extracting the .aar file.
b. If you choose to use your own build of the Keyman Engine, get the library from **android/Samples/KMSample1/app/libs/keyman-engine.aar**
a. We recommend [downloading](https://keyman.com/downloads/#android-engine)
the the latest stable release of Keyman Engine and extracting the .aar file.
b. If you choose to use your own build of the Keyman Engine, get the library
from **android/Samples/KMSample1/app/libs/keyman-engine.aar**
2. Open your project in Android Studio.
3. Open **build.gradle** (Module: app) in "Gradle Scripts".
4. Check that the `android{}` object, includes the following:
```gradle
android {
compileSdkVersion 35
// Don't compress kmp files so they can be copied via AssetManager
aaptOptions {
noCompress "kmp"
}
```gradle
android {
compileSdkVersion 36
// Don't compress kmp files so they can be copied via AssetManager
aaptOptions {
noCompress "kmp"
}
buildFeatures {
buildConfig = true
}
```
buildFeatures {
buildConfig = true
}
```
5. After the `android {}` object, include the following:
````gradle
repositories {
flatDir {
dirs 'libs'
````gradle
repositories {
flatDir {
dirs 'libs'
}
google()
mavenCentral()
}
google()
mavenCentral()
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
implementation 'com.google.android.material:material:1.12.0'
api (name:'keyman-engine', ext:'aar')
implementation 'androidx.preference:preference:1.2.1'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
implementation 'com.google.android.material:material:1.12.0'
api (name:'keyman-engine', ext:'aar')
implementation 'androidx.preference:preference:1.2.1'
// Include this if you want to have QR Codes displayed on Keyboard Info
implementation ('com.github.kenglxn.QRGen:android:3.0.1') {
transitive = true
// Include this if you want to have QR Codes displayed on Keyboard Info
implementation ('com.github.kenglxn.QRGen:android:3.0.1') {
transitive = true
}
}
}
````
````
5. include `import com.keyman.engine.*;` to use Keyman Engine in a class.
6. include `import com.keyman.engine.*;` to use Keyman Engine in a class.
### Keyman Engine for Android help content
Keyman Engine for Android help is maintained in the Markdown files in android/docs/engine/.
## Design Documentation
Internal design documents about features pertaining to Keyman for Android and Keyman Engine for Android are maintained in the Markdown files in android/docs/internal/.
Internal design documents about features pertaining to Keyman for Android and
Keyman Engine for Android are maintained in the Markdown files in android/docs/internal/.

View file

@ -12,7 +12,7 @@ java {
}
android {
compileSdk 35
compileSdk 36
namespace="com.keyman.kmsample1"
// Don't compress kmp files so they can be copied via AssetManager
@ -27,7 +27,7 @@ android {
defaultConfig {
applicationId "com.keyman.kmsample1"
minSdkVersion 21
targetSdkVersion 35
targetSdkVersion 36
// KEYMAN_VERSION_CODE, KEYMAN_VERSION_NAME, KEYMAN_VERSION_FOR_FILENAME from version.gradle
versionCode KEYMAN_VERSION_CODE as Integer

View file

@ -12,7 +12,7 @@ java {
}
android {
compileSdk 35
compileSdk 36
namespace="com.keyman.kmsample2"
// Don't compress kmp files so they can be copied via AssetManager
@ -27,7 +27,7 @@ android {
defaultConfig {
applicationId "com.keyman.kmsample2"
minSdkVersion 21
targetSdkVersion 35
targetSdkVersion 36
// KEYMAN_VERSION_CODE, KEYMAN_VERSION_NAME, KEYMAN_VERSION_FOR_FILENAME from version.gradle
versionCode KEYMAN_VERSION_CODE as Integer

View file

@ -12,7 +12,7 @@ java {
}
android {
compileSdk 35
compileSdk 36
namespace="com.keyman.android.tests.keyboardHarness"
// Don't compress kmp files so they can be copied via AssetManager
@ -27,7 +27,7 @@ android {
defaultConfig {
applicationId "com.keyman.android.tests.keyboardHarness"
minSdkVersion 21
targetSdkVersion 35
targetSdkVersion 36
// KEYMAN_VERSION_CODE, KEYMAN_VERSION_NAME, KEYMAN_VERSION_FOR_FILENAME from version.gradle
versionCode KEYMAN_VERSION_CODE as Integer

View file

@ -6,7 +6,7 @@ ext.rootPath = '../../../'
apply from: "$rootPath/version.gradle"
android {
compileSdk 35
compileSdk 36
namespace="com.keyman.android.tests.keycode"
buildFeatures {
@ -17,7 +17,7 @@ android {
defaultConfig {
applicationId "com.keyman.android.tests.keycode"
minSdkVersion 21
targetSdkVersion 35
targetSdkVersion 36
// KEYMAN_VERSION_CODE and KEYMAN_VERSION_NAME from version.gradle
versionCode KEYMAN_VERSION_CODE as Integer

View file

@ -3,11 +3,13 @@ title: KMManager.getAssociatedLexicalModel()
---
## Summary
The **getAssociatedLexicalModel()** method searches the installed lexical models list and see if there's an associated model for a given language
## Syntax
```java
HashMap<String, String> getAssociatedLexicalModel(String langId)
HashMap<String, String> getAssociatedLexicalModel(String langId)
```
### Parameters
@ -16,17 +18,23 @@ HashMap<String, String> getAssociatedLexicalModel(String langId)
: The language ID
### Returns
A dictionary of the associated lexical model information with keys and values defined as
A dictionary of the associated lexical model information with keys and values defined as
`HashMap<String key, String value)`.
`null` if there's no associated lexical model for a language `langId`.
If there are more than one lexical model installed for the given language,
the first one found will be returned.
## Description
Use this method to search the installed lexical models list and see if there's an associated model for a given language ID.
## Examples
### Example: Using `getAssociatedLexicalModel()`
The following script illustrates the use of `getAssociatedLexicalModel()`:
```java
@ -35,5 +43,6 @@ The following script illustrates the use of `getAssociatedLexicalModel()`:
```
## See also
* [addLexicalModel()](addLexicalModel)
* [registerLexicalModel()](registerLexicalModel)

View file

@ -125,8 +125,16 @@ namespace km {
{
assert(state);
assert(action_item);
if ((!state) || (!action_item))
if ((!state) || (!action_item)) {
return false;
}
// For this mock processor we only support queuing PERSIST_OPT action items.
if (action_item->type == KM_CORE_IT_PERSIST_OPT && action_item->option) {
state->actions().push_persist(update_option(static_cast<km_core_option_scope>(action_item->option->scope),
action_item->option->key,
action_item->option->value));
return true;
}
return false;
}
@ -166,6 +174,18 @@ namespace km {
u"F2 pressed test save."));
break;
}
case KM_CORE_VKEY_F3:
{
state->actions().push_persist(
update_option(KM_CORE_OPT_KEYBOARD,
u"__test_point_3",
u"F3 pressed test save 1."));
state->actions().push_persist(
update_option(KM_CORE_OPT_KEYBOARD,
u"__test_point_4",
u"F3 pressed test save 2."));
break;
}
case KM_CORE_VKEY_F4:
state->context().push_marker(KM_CORE_VKEY_QUOTE);

View file

@ -36,6 +36,23 @@ void actions::push_capslock(bool turnOn) {
emplace_back(std::move(ai));
}
actions::actions(actions const &other)
: std::vector<action>(other)
, _option_items_stack(other._option_items_stack)
{
// Update all option pointers to point to the new stack.
size_t opt_index = 0;
for (auto &item : *this) {
if (item.type == KM_CORE_IT_PERSIST_OPT) {
if (opt_index < _option_items_stack.size()) {
item.option = &_option_items_stack[opt_index++];
} else {
// no matching item in the stack; clear pointer.
item.option = nullptr;
}
}
}
}
state::state(km::core::abstract_processor & ap, km_core_option_item const *env)
: _processor(ap)

View file

@ -10,6 +10,7 @@
#include <cassert>
#include <vector>
#include <deque>
#include "keyman_core.h"
@ -27,7 +28,7 @@ using action = km_core_action_item;
class actions : public std::vector<action>
{
std::vector<option> _option_items_stack;
std::deque<option> _option_items_stack;
template<km_core_action_type V>
void _push_vkey(km_core_virtual_key);
@ -35,6 +36,10 @@ class actions : public std::vector<action>
public:
template<typename... Args>
actions(Args&&... args);
actions(actions const &other);
// If the operator is needed in the future, it shall be implemented.
// Currently blocking accidental use of it for now.
actions &operator=(actions const &) = delete;
void push_character(km_core_usv usv);
void push_marker(uint32_t marker);

View file

@ -251,6 +251,24 @@ constexpr km_core_option_item const expected_persist_opt = {
KM_CORE_OPT_KEYBOARD
};
constexpr km_core_option_item const clone_persist_opt = {
u"__test_clone",
u"Not in original",
KM_CORE_OPT_KEYBOARD
};
constexpr km_core_option_item const test_point_3_opt = {
u"__test_point_3",
u"F3 pressed test save 1.",
KM_CORE_OPT_KEYBOARD
};
constexpr km_core_option_item const test_point_4_opt = {
u"__test_point_4",
u"F3 pressed test save 2.",
KM_CORE_OPT_KEYBOARD
};
extern "C"
{
uint8_t test_imx_callback(km_core_state *state, uint32_t imx_id, void *callback_object){
@ -269,7 +287,8 @@ int main(int argc, char * argv[])
km_core_keyboard * test_kb = nullptr;
km_core_state * test_state = nullptr,
* test_clone = nullptr;
* test_clone = nullptr,
* test_clone_2 = nullptr;
test_kb = (km_core_keyboard *)new km::core::mock_processor(km::core::path("dummy.mock"));
// Simple sanity tests.
@ -394,9 +413,41 @@ int main(int argc, char * argv[])
clone_state_deleted_text
));
// Add two actions before cloning the state again
try_status(km_core_process_event(test_state, KM_CORE_VKEY_F3, 0, 1, KM_CORE_EVENT_FLAG_DEFAULT));
try_status(km_core_state_clone(test_state, &test_clone_2));
// Now put an option in the test_clone_2 state only
km_core_action_item action_clone = {KM_CORE_IT_PERSIST_OPT, {0,}, };
action_clone.option = &clone_persist_opt;
if (test_clone_2->actions().back().type == KM_CORE_IT_END) {
test_clone_2->actions().pop_back();
}
km_core_state_queue_action_items(test_clone_2, &action_clone);
test_clone_2->actions().commit();
// Test debug dump
auto doc3 = get_json_doc(*test_state), doc4 = get_json_doc(*test_clone_2);
std::cout << "doc3:" << std::endl;
std::cout << doc3 << std::endl;
std::cout << "doc4:" << std::endl;
std::cout << doc4 << std::endl;
if (doc3 == doc4) return __LINE__;
km_core_action_item action_tp3 = {KM_CORE_IT_PERSIST_OPT, {0,}, };
action_tp3.option = &test_point_3_opt;
km_core_action_item action_tp4 = {KM_CORE_IT_PERSIST_OPT, {0,}, };
action_tp4.option = &test_point_4_opt;
test_assert(action_items(test_state, {action_tp3, action_tp4, {KM_CORE_IT_END}}));
// Check that test_clone_2 has the same persisted options plus the extra queued option.
test_assert(action_items(test_clone_2, {action_tp3, action_tp4, action_clone, {KM_CORE_IT_END}}));
// Destroy them
km_core_state_dispose(test_state);
km_core_state_dispose(test_clone);
km_core_state_dispose(test_clone_2);
km_core_keyboard_dispose(test_kb);
return 0;

View file

@ -88,14 +88,14 @@
}
},
"node_modules/brace-expansion": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"version": "5.0.9",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
"integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
"node": "20 || >=22"
}
},
"node_modules/cacache": {
@ -642,9 +642,9 @@
}
},
"node_modules/tar": {
"version": "7.5.11",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.11.tgz",
"integrity": "sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==",
"version": "7.5.22",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz",
"integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==",
"dependencies": {
"@isaacs/fs-minipass": "^4.0.0",
"chownr": "^3.0.0",
@ -769,9 +769,9 @@
}
},
"brace-expansion": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"version": "5.0.9",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
"integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
"requires": {
"balanced-match": "^4.0.2"
}
@ -1166,9 +1166,9 @@
}
},
"tar": {
"version": "7.5.11",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.11.tgz",
"integrity": "sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==",
"version": "7.5.22",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz",
"integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==",
"requires": {
"@isaacs/fs-minipass": "^4.0.0",
"chownr": "^3.0.0",

View file

@ -2,7 +2,7 @@ import { EventEmitter } from "eventemitter3";
/**
* The return object documented for
* https://help.keyman.com/developer/engine/web/16.0/reference/core/getUIState.
* https://help.keyman.com/developer/engine/web/current-version/reference/core/getUIState.
*
* As it has long been documented in this format, property names should not be adjusted!
*/
@ -159,4 +159,4 @@ export class FocusAssistant extends EventEmitter<EventMap> {
this.focusing=false;
}, 50)
}
}
}

View file

@ -360,14 +360,13 @@ export class ContextManager extends ContextManagerBase<BrowserConfiguration> {
*
* This is based on the current `.activeTextStore` and its related attachment metadata.
*/
protected currentKeyboardSrcTextStore(): AbstractElementTextStore<any> {
protected currentKeyboardSrcTextStore(): AbstractElementTextStore<any> | null {
const textStore = this.currentTextStore || this.mostRecentTextStore;
if(this.isTextStoreKeyboardIndependent(textStore)) {
return textStore;
} else {
return null;
}
return null;
}
private isTextStoreKeyboardIndependent(textStore: AbstractElementTextStore<any>): boolean {
@ -403,12 +402,13 @@ export class ContextManager extends ContextManagerBase<BrowserConfiguration> {
}
/**
* Allows setting a control to a specific keyboard that does not change if the active keyboard changes when other
* controls are active. Only activates the keyboard if the specified control represents the currently-active
* context.
* Allows setting a control to a specific keyboard that does not change if
* the active keyboard changes when other controls are active. Only
* activates the keyboard if the specified control represents the
* currently-active context.
*
* This is the core method that backs
* https://help.keyman.com/developer/engine/web/15.0/reference/core/setKeyboardForControl.
* https://help.keyman.com/developer/engine/web/current-version/reference/core/setKeyboardForControl.
* @param textStore
* @param kbdId
* @param langId
@ -561,13 +561,13 @@ export class ContextManager extends ContextManagerBase<BrowserConfiguration> {
* Description Stores the last active element's keyboard settings. Should be called
* whenever a KMW-enabled page element loses control.
*/
public _BlurKeyboardSettings(lastElem: HTMLElement, PInternalName?: string, PLgCode?: string): void {
private _BlurKeyboardSettings(lastElem: HTMLElement, internalName?: string, lgCode?: string): void {
let keyboardID = this.activeKeyboard ? this.activeKeyboard.keyboard.id : '';
let langCode = this.activeKeyboard?.metadata.langId;
if(PInternalName !== undefined && PLgCode !== undefined) {
keyboardID = PInternalName;
langCode = PLgCode;
if(internalName !== undefined && lgCode !== undefined) {
keyboardID = internalName;
langCode = lgCode;
}
if(lastElem && lastElem._kmwAttachment.keyboard != null) {
@ -585,7 +585,7 @@ export class ContextManager extends ContextManagerBase<BrowserConfiguration> {
* whenever a KMW-enabled page element gains control, but only once the prior
* element's loss of control is guaranteed.
*/
public _FocusKeyboardSettings(lastElem: HTMLElement, blockGlobalChange: boolean): void {
private _FocusKeyboardSettings(lastElem: HTMLElement, blockGlobalChange: boolean): void {
// Important pre-condition: the newly-focused element must be set as active.
const attachment = lastElem._kmwAttachment;
const global = this.globalKeyboard;
@ -606,7 +606,7 @@ export class ContextManager extends ContextManagerBase<BrowserConfiguration> {
* The return value indicates whether (true) or not (false) the calling event handler
* should be terminated immediately after the call.
*/
public _CommonFocusHelper(textStore: AbstractElementTextStore<any>): boolean {
private _CommonFocusHelper(textStore: AbstractElementTextStore<any>): boolean {
const {focusAssistant} = this;
const activeKeyboard = this.activeKeyboard?.keyboard;
@ -631,7 +631,7 @@ export class ContextManager extends ContextManagerBase<BrowserConfiguration> {
/**
* Respond to KeymanWeb-aware input element receiving focus
*/
public _ControlFocus = (e: FocusEvent): boolean => {
private _ControlFocus = (e: FocusEvent): boolean => {
// Step 1: determine the corresponding TextStore instance.
const textStore = textStoreForEvent(e);
if(!textStore) {
@ -656,7 +656,7 @@ export class ContextManager extends ContextManagerBase<BrowserConfiguration> {
/**
* Respond to KMW losing focus on event
*/
public _ControlBlur = (e: FocusEvent): boolean => {
private _ControlBlur = (e: FocusEvent): boolean => {
// Step 0: if we're in a state where loss-of-focus should be outright-ignored, bypass the handler entirely.
if(this.focusAssistant._IgnoreNextSelChange) {
@ -728,7 +728,7 @@ export class ContextManager extends ContextManagerBase<BrowserConfiguration> {
return true;
}
doChangeEvent(textStore: AbstractElementTextStore<any>) {
private doChangeEvent(textStore: AbstractElementTextStore<any>) {
if(textStore.changed) {
const event = new Event('change', {"bubbles": true, "cancelable": false});
textStore.getElement().dispatchEvent(event);
@ -737,7 +737,7 @@ export class ContextManager extends ContextManagerBase<BrowserConfiguration> {
textStore.changed = false;
}
public _Click: (e: MouseEvent) => boolean = (e: MouseEvent) => {
private _Click: (e: MouseEvent) => boolean = (e: MouseEvent) => {
this.resetContext();
return true;
};
@ -839,4 +839,4 @@ export class ContextManager extends ContextManagerBase<BrowserConfiguration> {
this.page.shutdown();
this.domEventTracker.shutdown();
}
}
}

View file

@ -152,7 +152,7 @@ export class ContextManager extends ContextManagerBase<WebviewConfiguration> {
* Reflects the active 'textStore' upon which any `set activeKeyboard` operation will take place.
* For app/webview... there's only one textStore, thus only a "global default" matters.
*/
protected currentKeyboardSrcTextStore(): SyntheticTextStore {
protected currentKeyboardSrcTextStore(): SyntheticTextStore | null {
return null;
}
@ -223,4 +223,4 @@ export class ContextManager extends ContextManagerBase<WebviewConfiguration> {
super.resetContext();
this._hostTextStore.saveState();
}
}
}

View file

@ -4,12 +4,12 @@ export class AttachmentInfo {
/**
* Tracks the language code corresponding to the `keyboard` field.
*/
languageCode: string;
public languageCode: string;
/**
* Tracks the inputmode originally set by the webpage.
*/
inputMode?: string;
public inputMode?: string;
/**
* Constructor for AttachmentInfo.
@ -18,4 +18,4 @@ export class AttachmentInfo {
* @param keyboard - Provides the keyboard identifier.
*/
constructor(public readonly textStore: AbstractElementTextStore<any>, public keyboard: string) {}
}
}

View file

@ -50,16 +50,20 @@ export class CookieSerializer<Type extends Record<keyof Type, DecodedCookieField
*/
private _loadRawCookies(): Record<string, string> {
const v: Record<string, string> = {};
if(typeof(document.cookie) != 'undefined' && document.cookie != '') {
const c = document.cookie.split(/;\s*/);
for(let i = 0; i < c.length; i++) {
const d = c[i].split('=');
if(d.length == 2) {
v[d[0]] = d[1];
try {
if(typeof(document.cookie) != 'undefined' && document.cookie != '') {
const c = document.cookie.split(/;\s*/);
for(let i = 0; i < c.length; i++) {
const d = c[i].split('=');
if(d.length == 2) {
v[d[0]] = d[1];
}
}
}
} catch (e) {
// Ignore exceptions, e.g. if third-party cookies are blocked in
// the browser (#r.keymanweb.com/9)
}
return v;
}
@ -110,6 +114,11 @@ export class CookieSerializer<Type extends Record<keyof Type, DecodedCookieField
const d = new Date(new Date().valueOf() + 1000 * 60 * 60 * 24 * 30).toUTCString();
const cookieConfig = ' path=/; expires=' + d; //Fri, 31 Dec 2099 23:59:59 GMT;';
document.cookie = `${cookieName}=${encodeURIComponent(serialization)}; ${cookieConfig}`;
try {
document.cookie = `${cookieName}=${encodeURIComponent(serialization)}; ${cookieConfig}`;
} catch (e) {
// Ignore exceptions, e.g. if third-party cookies are blocked in
// the browser (#r.keymanweb.com/9)
}
}
}
}

View file

@ -150,7 +150,7 @@ export abstract class ContextManagerBase<MainConfig extends EngineConfiguration>
* attached elements within the app/browser textStore. For `app/webview`, this should
* always return a consistent value - likely, `null`.
*/
protected abstract currentKeyboardSrcTextStore(): TextStore;
protected abstract currentKeyboardSrcTextStore(): TextStore | null;
/**
* Ensures that newly activated keyboards are set correctly within managed context, possibly
@ -405,4 +405,4 @@ export abstract class ContextManagerBase<MainConfig extends EngineConfiguration>
}
}
}
}
}

View file

@ -330,10 +330,6 @@ export class KeymanEngineBase<
this.config.deferForInitialization.then(eventRaiser);
}
});
this.keyboardRequisitioner.cache.on('keyboardadded', (keyboard) => {
this.legacyAPIEvents.callEvent('keyboardloaded', { keyboardName: keyboard.id });
});
//
// #endregion

View file

@ -43,7 +43,7 @@ export type OSKRect = {
/**
* Definition for OSK events documented at
* https://help.keyman.com/developer/engine/web/16.0/reference/events/.
* https://help.keyman.com/developer/engine/web/current-version/reference/events/.
*/
export interface LegacyOSKEventMap {
'configclick'(obj: {}): void;
@ -1331,4 +1331,4 @@ export abstract class OSKView
): void {
this.legacyEvents.removeEventListener(event, fn);
}
}
}