Skip to content

fix(android): show updates for lexical models - #16266

Merged
ermshiperete merged 4 commits into
masterfrom
fix/android/16226_update
Aug 6, 2026
Merged

fix(android): show updates for lexical models#16266
ermshiperete merged 4 commits into
masterfrom
fix/android/16226_update

Conversation

@ermshiperete

@ermshiperete ermshiperete commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

With this change the model info activity will show an available update to the lexical model.

PR partially drafted by AI (Gemini 3 Flash Preview).

Fixes: #16226
Build-bot: release:android

User Testing

Preparations

(If you have a newer version of the SIL Euro Latin keyboard or the nrc.en.mtnt model installed, uninstall them and install the older versions. Unfortunately you'll have to wait a day before running the tests because the update check runs only once a day, and if it was installed it'll already have checked for updates when you opened the app)

Tests

  • GROUP_OLDAPI: Android 12 or older (API < 33)
  • GROUP_NEWAPI: Android 13 or newer (API >= 33)

TEST_KM_MODEL: verify adding a model for Khmer works

  • Go to Settings → Installed Languages → Central Khmer (Khmer, Cambodia) → Dictionary
  • Verify that the list shows two available dictionaries (Chuon Nath Dictionary and Khmer Google Crawler)
  • install one of them by clicking on it
  • verify that the dictionary now shows a checkbox in front
  • verify that clicking the right arrow shows the dictionary details

TEST_EN_KBD: verify that an update is available for the EuroLatin (SIL) keyboard

  • Go to Settings → Installed Languages → English → EuroLatin (SIL)
  • Verify the keyboard version (3.0.0) and it shows an available update

TEST_EN_MODEL: verify that an update is available for the dictionary

  • Go to Settings → Installed Languages → English → Dictionary → English dictionary (MTNT)
  • Verify the model version (0.3.0) and it shows an available update

TEST_UPDATE: verify installing updates

  • Verify that the menu offers the "Install Updates" item
  • click on "Install Updates"
  • confirm the update dialogs
  • verify that the EuroLatin keyboard and English dictionary have been updated

@github-project-automation github-project-automation Bot moved this to Todo in Keyman Jul 20, 2026
@keymanapp-test-bot keymanapp-test-bot Bot added the user-test-missing User tests have not yet been defined for the PR label Jul 20, 2026
@keymanapp-test-bot

keymanapp-test-bot Bot commented Jul 20, 2026

Copy link
Copy Markdown

User Test Results

Test specification and instructions

Test Artifacts

@keymanapp-test-bot keymanapp-test-bot Bot added this to the A19S34 milestone Jul 20, 2026
@ermshiperete
ermshiperete force-pushed the fix/android/16226_update branch from 8c5b51a to 0bc6a38 Compare July 21, 2026 10:26
@keymanapp-test-bot keymanapp-test-bot Bot added has-user-test user-test-required User tests have not been completed and removed user-test-missing User tests have not yet been defined for the PR labels Jul 21, 2026
@ermshiperete
ermshiperete force-pushed the fix/android/16226_update branch 2 times, most recently from 88e18d5 to f1adb22 Compare July 22, 2026 11:26
@ermshiperete
ermshiperete force-pushed the fix/android/16226_update branch from f1adb22 to ad1fbc2 Compare July 23, 2026 10:29
With this change the model info activity will show an available update
to the lexical model.

Commit partially drafted by AI (Gemini 3 Flash Preview).

Fixes: #16226
Build-bot: release:android
@ermshiperete
ermshiperete force-pushed the fix/android/16226_update branch from ad1fbc2 to a332c0d Compare July 23, 2026 11:18
@ermshiperete
ermshiperete marked this pull request as ready for review July 23, 2026 11:28
@ermshiperete
ermshiperete requested a review from jahorton July 23, 2026 11:28
@Meng-Heng

Meng-Heng commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

I am getting this error when dropping the apk file into the Android Emulator:

Error code: 'INSTALL_PARSE_FAILED_NO_CERTIFICATES', message='INSTALL_PARSE_FAILED_NO_CERTIFICATES: Failed to collect certificates from /data/app/vmdl2004119985.tmp/keyman-19.0.260-alpha-test-16266.apk: Attempt to get length of null array'

Can you help check?

Updated:
Emulator: Pixel 9 Android version 15.0 API 35

@Meng-Heng

Copy link
Copy Markdown
Contributor

Physical device: Galaxy A05, Android v15 One UI v7.0
Screenshot_20260729_141948_Package installer

@Meng-Heng

Meng-Heng commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Test Results

GROUP_OLDAPI:

Test Specs

  1. Android Emulator: Pixel 4, Android v12.0, API 31
  2. Keyman for Android v19.0.260-alpha-test-16266

Test Prerequisites

  1. Uninstall EuroLatin (SIL) keyboard (and its dictionary) after launching Keyman
  2. Install Khmer Angkor keyboard, and then delete the default Khmer Google Crawler dictionary
  3. Install the old versions of the EuroLatin (SIL) keyboard and model
  4. Dismiss and relaunch the app
  5. Dismiss the "Get Started" dialog
  • TEST_KM_MODEL (PASSED):
  1. Go to Settings → Installed Languages → Central Khmer (Khmer, Cambodia) → Dictionary
  2. VERIFIED: the list shows two available dictionaries (Chuon Nath Dictionary and Khmer Google Crawler)
  3. Install "Khmer Google Crawler Dictionary"
  4. VERIFIED: the dictionary shows a checkbox in front
  5. VERIFIED: clicking the right arrow shows the dictionary details
  6. Download both dictionaries and removing them work as expected.
  • TEST_EN_KBD (PASSED):
  1. Go to Settings → Installed Languages → English → EuroLatin (SIL)
  2. VERIFIED: No available update was shown
  3. Dismiss and reopen the app
  4. Go to Settings → Installed Languages → English → EuroLatin (SIL)
  5. VERIFIED: keyboard version (3.0.0) and it shows an available update
  • TEST_EN_MODEL (PASSED):
  1. Go to Settings → Installed Languages → English → Dictionary → English dictionary (MTNT)
  2. VERIFIED: model version (0.3.0) and it shows an available update.
  • TEST_UPDATE (PASSED):
  1. VERIFIED: the menu offers the "Install Updates" item
  2. Click on "Install Updates"
  3. Three Update dialogs for model appear and initiate
  4. Click on "Install Updates" again
  5. One Update dialog for keyboard appear and initiate
  6. VERIFIED: the EuroLatin keyboard and English dictionary have been updated.

GROUP_NEWAPI:

Test Specs

  1. Android Emulator: Pixel 9, Android v15.0, API 35
  2. Keyman for Android v19.0.260-alpha-test-16266

Test Prerequisites

  1. Uninstall EuroLatin (SIL) keyboard (and its dictionary) after launching Keyman
  2. Install Khmer Angkor keyboard, and then delete the default Khmer Google Crawler dictionary
  3. Dismiss the "Get Started" dialog
  • TEST_KM_MODEL (PASSED):
  1. Go to Settings → Installed Languages → Central Khmer (Khmer, Cambodia) → Dictionary
  2. VERIFIED: the list shows two available dictionaries (Chuon Nath Dictionary and Khmer Google Crawler)
  3. Install "Chuon Nath Dictionary"
  4. VERIFIED: the dictionary shows a checkbox in front
  5. VERIFIED: clicking the right arrow shows the dictionary details
  6. Download both dictionaries and removing them work as expected.
  • TEST_EN_KBD (PASSED):
  1. Install the old versions of the EuroLatin (SIL) keyboard and model
  2. Go to Settings → Installed Languages → English → EuroLatin (SIL)
  3. VERIFIED: No available update was shown
  4. Dismiss and reopen the app
  5. Go to Settings → Installed Languages → English → EuroLatin (SIL)
  6. VERIFIED: keyboard version (3.0.0) and it shows an available update
  • TEST_EN_MODEL (PASSED):
  1. Go to Settings → Installed Languages → English → Dictionary → English dictionary (MTNT)
  2. VERIFIED: model version (0.3.0) and it shows an available update
  • TEST_UPDATE (PASSED):
  1. VERIFIED: the menu offers the "Install Updates" item
  2. Click on "Install Updates"
  3. Three Update dialogs for model appear and initiate
  4. Click on "Install Updates" again
  5. One Update dialog for keyboard appear and initiate
  6. VERIFIED: the EuroLatin keyboard and English dictionary have been updated.

Note

  1. I did not wait a day to get the "Install Updates". The "Install Updates" for keyboard does not show up until I dismissed the app, whereas the model update shows "Update available" upon installing.
Screen.Recording.2026-07-31.at.11.07.33.in.the.morning.mp4
  1. Hopefully, the "Install Updates" in the video is showing the expected behavior because I had to click "Install Updates" twice for both updates.

I am pointing these out because the test cases and test results are a bit mismatched so I had to do some workaround.

@keymanapp-test-bot keymanapp-test-bot Bot removed the user-test-required User tests have not been completed label Jul 31, 2026
@keyman-server keyman-server modified the milestones: A19S34, A19S35 Aug 3, 2026

@jahorton jahorton left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels like there's extra work being included here past what's actually needed for a fix. Additional documentation may help further clarify the reasoning behind all of the changes.

Noting what a devin.ai review puts out:

This PR fixes the Android app to properly detect and display available updates for lexical models. The problem was multi-faceted:

  1. The processLexicalModelPackageUpdateJSON call was missing from the cloud catalog download callback, so model updates were never checked.
  2. The update check logic used a wrong version key (KMKey_Version instead of KMKey_LexicalModelVersion) and only looked up a single model by index instead of iterating all matching models.
  3. The model info activity didn't receive enough data to show update status because LanguageSettingsActivity wasn't passing the associated lexical model info.
  4. When merging cloud catalog models with installed models, addAll was used instead of a proper version-aware merge, causing duplicates or stale data.

From my current view, the actual core bug here was probably item 4. If an old version remains in the list, and comes first, it makes sense that the new version's entry never got reached. Items 1 and 2 do appear to support this as well.

I can see reasoning for item 3: if the cloud query operations suddenly decide that a different lexical model should be default for a language offering two or more, that could certainly pose an issue. Basically, when the user has one installed on-device, consider that one. The thing is... this case isn't currently being clearly or explicitly user-tested or unit-tested, so far as I can see, it wasn't noted in the base issue, and there's little in the PR description noting this aspect.

I would want to see, at minimum, actual user and/or unit testing for the case actually necessitating item 3 - the changes in how LanguageSettingsActivity passes data to ModelPickerActivity, in particular. I'd prefer spinning it off into a separate commit, if not PR and issue, unless it can be clearly shown to be necessary for a fix even for languages supported by a single model.

i.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
i.putExtras(bundle);
startActivity(i);
HashMap<String, String> lmInfo = KMManager.getAssociatedLexicalModel(lgCode);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, it looks like we're adding an alternate way to set the lexical model's data in place... but preserving the old one?

Are there any reasonable conditions for the getAssociatedLexicalModel call to fail? Why are we keeping the bundle.putString approach around, if not?

What advantages does the new approach offer? If the answer is mostly just "it's cleaner"... then why does this not clean up related code on the other side in ModelPickerActivity?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I should have reverted this change. It's not necessary for this PR. I started out to do the cleanup but then discovered that it would require too many changes not related to fixing this bug, so I didn't continue the cleanup. As part of my exploration why it's not working I tried to make it more similar to how it's done for the keyboards.

Comment on lines +73 to +76
HashMap<String, String> 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, "");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This appears to be the "other side" of the previously-noted "alternate way" comment. The logic here is complexified due to selecting between two approaches, where just one appeared to work previously.

downloadMetaDataFromServer(context,updateHandler,onSuccess,onFailure);
}

private void mergeLexicalModels(Dataset.LexicalModels datasetModels, List<LexicalModel> newModels) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doc-comments, please.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

}
if (kmpLexicalModelsArray.length() > 0) {
memCachedDataset.lexicalModels.addAll(CloudDataJsonUtil.processLexicalModelJSON(kmpLexicalModelsArray, fromKMP));
mergeLexicalModels(memCachedDataset.lexicalModels, CloudDataJsonUtil.processLexicalModelJSON(kmpLexicalModelsArray, true));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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 installedSet above DO exist - the set is based on the locally installed models. Here we add all models that exist locally - whether installed or not. The difference to installedSet is that only installedSet has the download link for the new version.

It's possible that just using installedSet would be sufficient - at least it works with just that, but I don't know which edge cases that would break, i.e. what we would loose if models that exit locally but are not installed wouldn't be in the dataset.

i.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
i.putExtras(bundle);
startActivity(i);
HashMap<String, String> lmInfo = KMManager.getAssociatedLexicalModel(lgCode);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I should have reverted this change. It's not necessary for this PR. I started out to do the cleanup but then discovered that it would require too many changes not related to fixing this bug, so I didn't continue the cleanup. As part of my exploration why it's not working I tried to make it more similar to how it's done for the keyboards.

Comment on lines +228 to +231
LexicalModel cached = CloudRepository.shared.getLexicalModel(aContext, languageID, lexicalModelID);
if (cached != null) {
cached.setUpdateKMP(link);
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lines 228-231 are not necessary. Left-over from exploration.

}
if (kmpLexicalModelsArray.length() > 0) {
memCachedDataset.lexicalModels.addAll(CloudDataJsonUtil.processLexicalModelJSON(kmpLexicalModelsArray, fromKMP));
mergeLexicalModels(memCachedDataset.lexicalModels, CloudDataJsonUtil.processLexicalModelJSON(kmpLexicalModelsArray, true));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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 installedSet above DO exist - the set is based on the locally installed models. Here we add all models that exist locally - whether installed or not. The difference to installedSet is that only installedSet has the download link for the new version.

It's possible that just using installedSet would be sufficient - at least it works with just that, but I don't know which edge cases that would break, i.e. what we would loose if models that exit locally but are not installed wouldn't be in the dataset.

@ermshiperete

Copy link
Copy Markdown
Contributor Author

Yes, sorry for not better documenting the different changes. The commit/PR description should have been more extensive.

I went through again and checked what's really necessary to fix the issue. Turns out most of the changes are needed. There were a few that are unnecessary, so I reverted those (particular related to item 3).

@ermshiperete
ermshiperete force-pushed the fix/android/16226_update branch from a0b0738 to 6e83adb Compare August 5, 2026 16:23
* @return LexicalModel of an associated lexical model. Null if no match found
*/
public LexicalModel getAssociatedLexicalModel(@NonNull Context context, String languageID) {
return getLexicalModel(context, languageID, null);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See #16308

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);

Copy link
Copy Markdown
Contributor Author

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.

@ermshiperete
ermshiperete merged commit c41544e into master Aug 6, 2026
7 checks passed
@ermshiperete
ermshiperete deleted the fix/android/16226_update branch August 6, 2026 14:15
@github-project-automation github-project-automation Bot moved this from Todo to Done in Keyman Aug 6, 2026
@keyman-server

Copy link
Copy Markdown
Collaborator

Changes in this pull request will be available for download in Keyman version 19.0.262-alpha

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

bug(android): lexical model did not update automatically

4 participants