diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/LanguageSettingsActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/LanguageSettingsActivity.java index 9342a0b528..97dee3c7fc 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/LanguageSettingsActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/LanguageSettingsActivity.java @@ -159,14 +159,19 @@ public final class LanguageSettingsActivity extends BaseActivity { @Override public void onClick(View v) { // Start ModelPickerActivity + Intent intent = new Intent(context, ModelPickerActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); Bundle bundle = new Bundle(); - bundle.putString(KMManager.KMKey_LanguageID, lgCode); - bundle.putString(KMManager.KMKey_LanguageName, lgName); - bundle.putString(KMManager.KMKey_CustomHelpLink, customHelpLink); - Intent i = new Intent(context, ModelPickerActivity.class); - i.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); - i.putExtras(bundle); - startActivity(i); + HashMap lmInfo = KMManager.getAssociatedLexicalModel(lgCode); + if (lmInfo != null) { + bundle.putSerializable(KMManager.KMKey_LexicalModel, lmInfo); + } else { + bundle.putString(KMManager.KMKey_LanguageID, lgCode); + bundle.putString(KMManager.KMKey_LanguageName, lgName); + bundle.putString(KMManager.KMKey_CustomHelpLink, customHelpLink); + } + intent.putExtras(bundle); + startActivity(intent); } }); 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/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/ModelPickerActivity.java b/android/KMEA/app/src/main/java/com/keyman/engine/ModelPickerActivity.java index 4595aae1e7..3451b8ec83 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/ModelPickerActivity.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/ModelPickerActivity.java @@ -19,6 +19,7 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; +import androidx.core.os.BundleCompat; import com.keyman.engine.cloud.CloudApiTypes; import com.keyman.engine.cloud.CloudDownloadMgr; @@ -69,14 +70,15 @@ public final class ModelPickerActivity extends BaseActivity { TextView textView = (TextView) findViewById(R.id.bar_title); Bundle bundle = getIntent().getExtras(); - String newLanguageID = bundle.getString(KMManager.KMKey_LanguageID); - String newCustomHelpLink = bundle.getString(KMManager.KMKey_CustomHelpLink, ""); + HashMap lmInfo = BundleCompat.getSerializable(bundle, KMManager.KMKey_LexicalModel, HashMap.class); + final String newLanguageID = lmInfo != null ? lmInfo.get(KMManager.KMKey_LanguageID) : bundle.getString(KMManager.KMKey_LanguageID, ""); + final String newCustomHelpLink = lmInfo != null ? lmInfo.get(KMManager.KMKey_CustomHelpLink) : bundle.getString(KMManager.KMKey_CustomHelpLink, ""); + final String languageName = lmInfo != null ? lmInfo.get(KMManager.KMKey_LanguageName) : bundle.getString(KMManager.KMKey_LanguageName, ""); // Sometimes we need to re-initialize the list of models that are displayed in the ListView languageID = newLanguageID; customHelpLink = newCustomHelpLink; - final String languageName = bundle.getString(KMManager.KMKey_LanguageName); textView.setText(String.format(getString(R.string.model_picker_header), languageName)); listView = (ListView) findViewById(R.id.listView); 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..b44ad509cb 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,29 @@ 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; + LexicalModel cached = CloudRepository.shared.getLexicalModel(aContext, languageID, lexicalModelID); + if (cached != null) { + cached.setUpdateKMP(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 +376,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 +335,13 @@ public class CloudRepository { languageCodes.add(installedSet.getItem(i).code); } + // add all models from installed set + 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 +349,7 @@ public class CloudRepository { memCachedDataset.keyboards.addAll(CloudDataJsonUtil.processKeyboardJSON(kmpLanguagesArray, true)); } if (kmpLexicalModelsArray.length() > 0) { - memCachedDataset.lexicalModels.addAll(CloudDataJsonUtil.processLexicalModelJSON(kmpLexicalModelsArray, fromKMP)); + 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..e0ac4dfad1 --- /dev/null +++ b/android/KMEA/app/src/test/java/com/keyman/engine/data/CloudRepositoryTests.java @@ -0,0 +1,98 @@ +/* + * 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.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +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; + } + + @Before + public void setUp() { + context = ApplicationProvider.getApplicationContext(); + repository = CloudRepository.shared; + dataset = new Dataset(context); + } + + @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()); + } +}