Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
8234092
feat: contract-driven dynamic database download from API
catreedle Feb 10, 2026
d49a99c
feat: persist download states across app relaunches
catreedle Feb 11, 2026
ed6e803
fix: fix installed keyboards not showing in Settings
catreedle Feb 11, 2026
5e2c593
feat: trigger download and "Downloading" state from Select Translatio…
catreedle Feb 11, 2026
88f8148
redirect to Download Screen on confirming translation source change
catreedle Feb 11, 2026
cf24823
feat: set Update state for download button using data version endpoint
catreedle Feb 12, 2026
d645f83
fix download Toast display message
catreedle Feb 13, 2026
14f2ebb
Add new YAML based contract files
andrewtavis Feb 15, 2026
f23e677
Finalize form of YAML contracts
andrewtavis Feb 15, 2026
3af7f7a
Fix included double quote in en.yaml
andrewtavis Feb 15, 2026
8dad758
Update version of data contracts with necessary fields
andrewtavis Feb 15, 2026
17626a2
Minor fix in comment in contracts
andrewtavis Feb 15, 2026
430c058
feat: change to using YAML for data contract
catreedle Feb 16, 2026
8d0d370
remove json contracts
catreedle Feb 16, 2026
19e6fc7
fix minor typo
catreedle Feb 16, 2026
f5b6ff0
feat: read from new db and check for table and column existence
catreedle Feb 17, 2026
1bef6c9
Merge branch 'main' into read-new-db
catreedle Mar 2, 2026
7944c77
fix minor import
catreedle Mar 2, 2026
cac5b54
fix build error from merge conflict
catreedle Mar 2, 2026
704507b
fix merge conflict duplicate functions
catreedle Mar 2, 2026
708b80b
remove Download All state and fix crashes
catreedle Mar 3, 2026
c9b8e7b
fix "Check for new data" as spinner
catreedle Mar 4, 2026
6bd2f0e
fix avoid action once Check for new data is done
catreedle Mar 5, 2026
5265b9f
feat: MVP autocompletions fallback using nouns table
catreedle Mar 6, 2026
e021b77
Merge branch 'main' into autocompletion-fallback
andrewtavis Mar 21, 2026
7024dfd
Merge branch 'main' into autocompletion-fallback
andrewtavis Mar 21, 2026
4d0f02d
Merge branch 'main' into autocompletion-fallback
andrewtavis Mar 21, 2026
a4a3436
Merge branch 'main' into autocompletion-fallback
catreedle Mar 24, 2026
dccd1ff
fix autocomplete capitalization
catreedle Mar 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 64 additions & 10 deletions app/src/main/java/be/scri/helpers/data/AutocompletionDataManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,59 @@
package be.scri.helpers.data

import be.scri.helpers.DatabaseFileManager
import be.scri.helpers.StringUtils.isWordCapitalized

/**
* This class manages the autocomplete system.
* It loads words from a language-specific SQLite database,
* and stores them in a Trie data structure for fast prefix-based lookup.
* If the `autocomplete_lexicon` table/Trie is not available, it falls back to caching all noun words.
*/
class AutocompletionDataManager(
private val fileManager: DatabaseFileManager,
) {
private val trie = Trie()
private var trieLoaded = false
private val nounWords = mutableListOf<String>()

/**
* Loads all words from the language-specific database into the trie.
* If the `autocomplete_lexicon` table/Trie is not present, it loads noun words from the specified columns instead.
*
* @param language The language code (e.g. "en", "id") for which to load words.
* @param numbersColumns Column names from the contract's `numbers` map.
*/
fun loadWords(language: String) {
fun loadWords(
language: String,
numbersColumns: List<String> = emptyList(),
) {
val db = fileManager.getLanguageDatabase(language) ?: return

db.use { database ->
if (!database.tableExists("autocomplete_lexicon")) return

database.rawQuery("SELECT word FROM autocomplete_lexicon", null).use { cursor ->
val wordIndex = cursor.getColumnIndex("word")
while (cursor.moveToNext()) {
val word = cursor.getString(wordIndex)?.lowercase()?.trim()
if (!word.isNullOrEmpty()) {
trie.insert(word)
if (database.tableExists("autocomplete_lexicon")) {
database.rawQuery("SELECT word FROM autocomplete_lexicon", null).use { cursor ->
val wordIndex = cursor.getColumnIndex("word")
while (cursor.moveToNext()) {
val word = cursor.getString(wordIndex)?.lowercase()?.trim()
if (!word.isNullOrEmpty()) {
trie.insert(word)
}
}
}
trieLoaded = true
} else if (database.tableExists("nouns") && numbersColumns.isNotEmpty()) {
val unionQuery =
numbersColumns.joinToString(" UNION ") { column ->
"SELECT DISTINCT $column AS word FROM nouns WHERE $column IS NOT NULL AND $column != ''"
} + " ORDER BY word ASC"

database.rawQuery(unionQuery, null).use { cursor ->
val wordIndex = cursor.getColumnIndex("word")
while (cursor.moveToNext()) {
val word = cursor.getString(wordIndex)?.lowercase()?.trim()
if (!word.isNullOrEmpty()) {
nounWords.add(word)
}
}
}
}
Expand All @@ -38,6 +63,7 @@ class AutocompletionDataManager(

/**
* Returns autocomplete suggestions for a given prefix.
* Uses the Trie if loaded, otherwise filters the cached noun word list.
*
* @param prefix The starting text to search for (e.g. "ap").
* @param limit The maximum number of suggestions to return (default: 3).
Expand All @@ -47,5 +73,33 @@ class AutocompletionDataManager(
fun getAutocompletions(
prefix: String,
limit: Int = 3,
): List<String> = trie.searchPrefix(prefix, limit)
): List<String> {
val isCapitalized = isWordCapitalized(prefix)

val results =
if (trieLoaded) {
trie.searchPrefix(prefix.lowercase().trim(), limit)
} else {
getAutocompletionsFromNouns(prefix.lowercase().trim(), limit)
}
return if (isCapitalized) {
results.map { it.replaceFirstChar { it.uppercaseChar() } }
} else {
results
}
}

/**
* Filters the cached noun word list to find matches that start with the given prefix.
*/
private fun getAutocompletionsFromNouns(
prefix: String,
limit: Int,
): List<String> {
if (nounWords.isEmpty() || prefix.isBlank()) return emptyList()
val normalizedPrefix = prefix.lowercase().trim()
return nounWords
.filter { it.startsWith(normalizedPrefix) }
.take(limit)
}
}
6 changes: 5 additions & 1 deletion app/src/main/java/be/scri/services/GeneralKeyboardIME.kt
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,11 @@ abstract class GeneralKeyboardIME(
?.toSet()
nounKeywords = dbManagers.genderManager.findGenderOfWord(languageAlias, dataContract)
suggestionWords = dbManagers.suggestionManager.getSuggestions(languageAlias)
autocompletionManager.loadWords(languageAlias)
val numbersColumns =
dataContract?.numbers?.let { map ->
(map.keys + map.values).distinct()
} ?: emptyList()
autocompletionManager.loadWords(languageAlias, numbersColumns)
caseAnnotation = dbManagers.prepositionManager.getCaseAnnotations(languageAlias)

val tempConjugateOutput = dbManagers.conjugateDataManager.getTheConjugateLabels(languageAlias, dataContract, "describe")
Expand Down
Loading