diff --git a/HISTORY.md b/HISTORY.md index 399e3077f2..e348de89f3 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -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) diff --git a/VERSION.md b/VERSION.md index 48c4023fd8..f00ee4d41a 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -19.0.259 \ No newline at end of file +19.0.263 \ No newline at end of file diff --git a/android/KMAPro/kMAPro/build.gradle b/android/KMAPro/kMAPro/build.gradle index cb5e52731a..7c6b966484 100644 --- a/android/KMAPro/kMAPro/build.gradle +++ b/android/KMAPro/kMAPro/build.gradle @@ -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 diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java index 89ad50d046..01f38a4a66 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java @@ -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()) { diff --git a/android/KMEA/app/build.gradle b/android/KMEA/app/build.gradle index 8f77d155f0..7a1844c2e0 100644 --- a/android/KMEA/app/build.gradle +++ b/android/KMEA/app/build.gradle @@ -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+"\"" diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/KeyboardPickerActivity.java b/android/KMEA/app/src/main/java/com/keyman/engine/KeyboardPickerActivity.java index 29cd0e61dc..283b858fd4 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/KeyboardPickerActivity.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/KeyboardPickerActivity.java @@ -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> getLexicalModelsMatchingId(Context context, String lexicalModelId) { + ArrayList> lexicalModels = new ArrayList<>(); + + if (lexicalModelsList == null) { + lexicalModelsList = getLexicalModelsList(context); + } + + if (lexicalModelsList != null) { + int length = lexicalModelsList.size(); + for (int i=0; i < length; i++) { + HashMap lmInfo = lexicalModelsList.get(i); + if (lexicalModelId.equals(lmInfo.get(KMManager.KMKey_LexicalModelID))) { + lexicalModels.add(lmInfo); + } + } + } + + return lexicalModels; + } + public static HashMap getLexicalModelInfo(Context context,int index) { if (index < 0) { return null; diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/cloud/CloudDataJsonUtil.java b/android/KMEA/app/src/main/java/com/keyman/engine/cloud/CloudDataJsonUtil.java index 46eb51f558..a29d937a83 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/cloud/CloudDataJsonUtil.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/cloud/CloudDataJsonUtil.java @@ -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 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 lmInfo = KeyboardPickerActivity.getLexicalModelInfo(aContext, index); - String version = lmInfo.get(KMManager.KMKey_Version); + ArrayList> lmInfos = KeyboardPickerActivity.getLexicalModelsMatchingId(aContext, lexicalModelID); + for (HashMap 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( diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/cloud/impl/CloudCatalogDownloadCallback.java b/android/KMEA/app/src/main/java/com/keyman/engine/cloud/impl/CloudCatalogDownloadCallback.java index 21769f76dd..a8fe4cc3e5 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/cloud/impl/CloudCatalogDownloadCallback.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/cloud/impl/CloudCatalogDownloadCallback.java @@ -154,6 +154,7 @@ public class CloudCatalogDownloadCallback implements ICloudDownloadCallback newModels) { + if (newModels == null) { + return; + } + + List existingModels = new ArrayList<>(datasetModels.asList()); + List 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); diff --git a/android/KMEA/app/src/test/java/com/keyman/engine/data/CloudRepositoryTests.java b/android/KMEA/app/src/test/java/com/keyman/engine/data/CloudRepositoryTests.java new file mode 100644 index 0000000000..b1796764be --- /dev/null +++ b/android/KMEA/app/src/test/java/com/keyman/engine/data/CloudRepositoryTests.java @@ -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 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 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 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")); + } +} diff --git a/android/README.md b/android/README.md index 76ebf90b22..36a3727075 100644 --- a/android/README.md +++ b/android/README.md @@ -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/. diff --git a/android/Samples/KMSample1/app/build.gradle b/android/Samples/KMSample1/app/build.gradle index 4643d0e796..f85a02b23a 100644 --- a/android/Samples/KMSample1/app/build.gradle +++ b/android/Samples/KMSample1/app/build.gradle @@ -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 diff --git a/android/Samples/KMSample2/app/build.gradle b/android/Samples/KMSample2/app/build.gradle index a3c9658056..44d67cca3f 100644 --- a/android/Samples/KMSample2/app/build.gradle +++ b/android/Samples/KMSample2/app/build.gradle @@ -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 diff --git a/android/Tests/KeyboardHarness/app/build.gradle b/android/Tests/KeyboardHarness/app/build.gradle index 070ee5c63b..079a716d6e 100644 --- a/android/Tests/KeyboardHarness/app/build.gradle +++ b/android/Tests/KeyboardHarness/app/build.gradle @@ -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 diff --git a/android/Tests/keycode/app/build.gradle b/android/Tests/keycode/app/build.gradle index 769cddb88a..86a9d1931e 100644 --- a/android/Tests/keycode/app/build.gradle +++ b/android/Tests/keycode/app/build.gradle @@ -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 diff --git a/android/docs/engine/KMManager/getAssociatedLexicalModel.md b/android/docs/engine/KMManager/getAssociatedLexicalModel.md index e92e6f3449..29a2ae2720 100644 --- a/android/docs/engine/KMManager/getAssociatedLexicalModel.md +++ b/android/docs/engine/KMManager/getAssociatedLexicalModel.md @@ -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 getAssociatedLexicalModel(String langId) +HashMap getAssociatedLexicalModel(String langId) ``` ### Parameters @@ -16,17 +18,23 @@ HashMap 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 `HashMaptype == KM_CORE_IT_PERSIST_OPT && action_item->option) { + state->actions().push_persist(update_option(static_cast(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); diff --git a/core/src/state.cpp b/core/src/state.cpp index 8eb99d1b0a..3bf9efb358 100644 --- a/core/src/state.cpp +++ b/core/src/state.cpp @@ -36,6 +36,23 @@ void actions::push_capslock(bool turnOn) { emplace_back(std::move(ai)); } +actions::actions(actions const &other) +: std::vector(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) diff --git a/core/src/state.hpp b/core/src/state.hpp index da2cf6bb75..15210a3a60 100644 --- a/core/src/state.hpp +++ b/core/src/state.hpp @@ -10,6 +10,7 @@ #include #include +#include #include "keyman_core.h" @@ -27,7 +28,7 @@ using action = km_core_action_item; class actions : public std::vector { - std::vector