-
-
Notifications
You must be signed in to change notification settings - Fork 143
fix(android): show updates for lexical models #16266
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a332c0d
e3ed30a
0368a7b
6e83adb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,6 +21,7 @@ | |
| 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; | ||
|
|
||
|
|
@@ -253,6 +254,49 @@ public void updateDatasetIfNeeded(@NonNull Context context, UpdateHandler update | |
| 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) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doc-comments, please.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
| 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,19 +330,24 @@ private void preCacheDataSet(@NonNull Context context, UpdateHandler updateHandl | |
| 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). | ||
| getJSONArray(KMKeyboardDownloaderActivity.KMKey_Languages).length() > 0) { | ||
| 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)); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I feel like this section could use some documentation. So... this step aims to reconcile previously-cached data with the new incoming data from the cloud? Rather than replace the old data wholesale, we preserve entries for models that appear to no longer exist? I do realize the old code was likely doing something similar here, but I feel like these lines would be well-served by comments providing additional context.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Honestly, I don't fully understand why we're doing this here. Since it was here before I kept it. All the models that we added from It's possible that just using |
||
| } | ||
| } catch (Exception e) { | ||
| KMLog.LogException(TAG, "preCacheDataSet error ", e); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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")); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change is necessary to get matching models. The way we did it before (
KeyboardPickerActivity.getLexicalModelIndex(aContext, lexicalModelID)) never found anything because the the second parameter is expected to be the full model key, consisting of pkg id, language id and model id, but we passed in only the model id.