From 876e5dbc8ec9b16a090c749bf9fbc183e6aabbba Mon Sep 17 00:00:00 2001 From: Prince Yadav <66916296+prince-0408@users.noreply.github.com> Date: Fri, 21 Aug 2026 14:54:35 +0530 Subject: [PATCH] refactor: extract KeyboardDataHandler from GeneralKeyboardIME (#426) --- .../be/scri/helpers/KeyboardDataHandler.kt | 152 ++++++++++++++++++ .../be/scri/services/GeneralKeyboardIME.kt | 125 +++++++------- .../scri/helpers/KeyboardDataHandlerTest.kt | 54 +++++++ 3 files changed, 272 insertions(+), 59 deletions(-) create mode 100644 app/src/keyboards/java/be/scri/helpers/KeyboardDataHandler.kt create mode 100644 app/src/testKeyboards/kotlin/be/scri/helpers/KeyboardDataHandlerTest.kt diff --git a/app/src/keyboards/java/be/scri/helpers/KeyboardDataHandler.kt b/app/src/keyboards/java/be/scri/helpers/KeyboardDataHandler.kt new file mode 100644 index 00000000..1086f223 --- /dev/null +++ b/app/src/keyboards/java/be/scri/helpers/KeyboardDataHandler.kt @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package be.scri.helpers + +import DataContract +import android.content.Context +import android.database.sqlite.SQLiteException +import android.util.Log +import be.scri.helpers.LanguageMappingConstants.getLanguageAlias +import be.scri.helpers.data.AutocompletionDataManager +import be.scri.helpers.english.ENInterfaceVariables.ALREADY_PLURAL_MSG + +/** + * Manages database initializations, data contract loading, dictionary queries, + * translations, plurals, and autocompletion data for the keyboard system. + */ +class KeyboardDataHandler { + lateinit var dbManagers: DatabaseManagers + private set + + lateinit var autocompletionManager: AutocompletionDataManager + private set + + var dataContract: DataContract? = null + internal set + + var emojiKeywords: HashMap>? = null + internal set + + var emojiMaxKeywordLength: Int = 0 + internal set + + var pluralWords: Set? = null + internal set + + lateinit var nounKeywords: HashMap> + internal set + + lateinit var suggestionWords: HashMap> + internal set + + lateinit var caseAnnotation: HashMap> + internal set + + var conjugateOutput: MutableMap>>? = null + internal set + + var conjugateLabels: Set = emptySet() + internal set + + val isInitialized: Boolean + get() = this::dbManagers.isInitialized + + /** + * Initializes the database managers using the given [context]. + */ + fun initialize(context: Context) { + dbManagers = DatabaseManagers(context) + autocompletionManager = dbManagers.autocompletionManager + } + + /** + * Loads dictionary, emoji, plural, gender, preposition, and conjugate data for [language]. + */ + fun loadLanguageData(language: String) { + val languageAlias = getLanguageAlias(language) + dataContract = dbManagers.getLanguageContract(languageAlias) + emojiKeywords = dbManagers.emojiManager.getEmojiKeywords(languageAlias) + emojiMaxKeywordLength = dbManagers.emojiManager.maxKeywordLength + pluralWords = + dbManagers.pluralManager + .getAllPluralForms(languageAlias, dataContract) + ?.map { it.lowercase() } + ?.toSet() + nounKeywords = dbManagers.genderManager.findGenderOfWord(languageAlias, dataContract) + suggestionWords = dbManagers.suggestionManager.getSuggestions(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") + conjugateOutput = if (tempConjugateOutput?.isEmpty() == true) null else tempConjugateOutput + conjugateLabels = dbManagers.conjugateDataManager.extractConjugateHeadings(dataContract, "coacha") + } + + /** + * Queries autocompletion suggestions for [prefix] up to [limit]. + */ + fun getAutocompletions( + prefix: String, + limit: Int = 3, + ): List = + try { + autocompletionManager.getAutocompletions(prefix, limit) + } catch (e: SQLiteException) { + Log.e("KeyboardDataHandler", "Database error in autocompletion", e) + emptyList() + } catch (e: IllegalStateException) { + Log.e("KeyboardDataHandler", "Illegal state in autocompletion", e) + emptyList() + } + + /** + * Retrieves the plural form of [word] for [language]. + */ + fun getPluralRepresentation( + language: String, + word: String?, + ): String? { + if (word.isNullOrEmpty()) return null + val langAlias = getLanguageAlias(language) + val lowercaseWord = word.lowercase() + if (pluralWords?.contains(lowercaseWord) == true) return ALREADY_PLURAL_MSG + return dbManagers.pluralManager + .getPluralRepresentation(langAlias, dataContract, word) + .values + .firstOrNull() + } + + /** + * Retrieves the translation for [commandBarInput] in [language]. + */ + fun getTranslation( + language: String, + commandBarInput: String, + ): String { + val sourceDest = dbManagers.translationDataManager.getSourceAndDestinationLanguage(language) + return dbManagers.translationDataManager.getTranslationDataForAWord(sourceDest, commandBarInput) + } + + /** + * Queries conjugate labels and headings for [searchInput] in [language]. + */ + fun queryConjugateData( + language: String, + searchInput: String, + ): Pair>>?, Set> { + val languageAlias = getLanguageAlias(language) + val tempOutput = dbManagers.conjugateDataManager.getTheConjugateLabels(languageAlias, dataContract, searchInput) + val output = + if (tempOutput?.isEmpty() == true || tempOutput?.values?.all { it.isEmpty() } == true) { + null + } else { + tempOutput + } + val labels = dbManagers.conjugateDataManager.extractConjugateHeadings(dataContract, searchInput) + return Pair(output, labels) + } +} diff --git a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt index 7b34094e..4f8d15f3 100644 --- a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt +++ b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt @@ -7,7 +7,6 @@ import android.R.color.white import android.content.Context import android.content.Intent import android.content.res.ColorStateList -import android.database.sqlite.SQLiteException import android.graphics.Color import android.graphics.Rect import android.graphics.drawable.GradientDrawable @@ -20,7 +19,6 @@ import android.text.InputType.TYPE_CLASS_DATETIME import android.text.InputType.TYPE_CLASS_NUMBER import android.text.InputType.TYPE_CLASS_PHONE import android.text.InputType.TYPE_MASK_CLASS -import android.util.Log import android.view.KeyEvent import android.view.View import android.view.inputmethod.EditorInfo @@ -50,6 +48,7 @@ import be.scri.helpers.DatabaseManagers import be.scri.helpers.EmojiUtils.insertEmoji import be.scri.helpers.FloatingKeyboardHandler import be.scri.helpers.KeyboardBase +import be.scri.helpers.KeyboardDataHandler import be.scri.helpers.KeyboardLanguageMappingConstants import be.scri.helpers.KeyboardStateManager import be.scri.helpers.LanguageMappingConstants.getLanguageAlias @@ -154,13 +153,24 @@ abstract class GeneralKeyboardIME( private val shiftPermToggleSpeed: Int = DEFAULT_SHIFT_PERM_TOGGLE_SPEED - private lateinit var dbManagers: DatabaseManagers + internal val dataHandler = KeyboardDataHandler() + + internal val dbManagers: DatabaseManagers + get() = dataHandler.dbManagers + + internal val autocompletionManager: AutocompletionDataManager + get() = dataHandler.autocompletionManager + private lateinit var nativeSuggestionEngine: NativeSuggestionEngine internal lateinit var suggestionHandler: SuggestionHandler internal lateinit var autocompletionHandler: AutocompletionHandler - private lateinit var autocompletionManager: AutocompletionDataManager internal val floatingKeyboardHandler by lazy { FloatingKeyboardHandler(this) } - private var dataContract: DataContract? = null + + internal var dataContract: DataContract? + get() = dataHandler.dataContract + set(value) { + dataHandler.dataContract = value + } internal val isUiManagerInitialized: Boolean get() = this::uiManager.isInitialized @@ -168,15 +178,53 @@ abstract class GeneralKeyboardIME( internal fun applyNavBarColorPublic() = applyNavBarColor() - var emojiKeywords: HashMap>? = null - private var conjugateOutput: MutableMap>>? = null - private var conjugateLabels: Set = emptySet() + var emojiKeywords: HashMap>? + get() = dataHandler.emojiKeywords + set(value) { + dataHandler.emojiKeywords = value + } + + private var conjugateOutput: MutableMap>>? + get() = dataHandler.conjugateOutput + set(value) { + dataHandler.conjugateOutput = value + } + + private var conjugateLabels: Set + get() = dataHandler.conjugateLabels + set(value) { + dataHandler.conjugateLabels = value + } + + private var emojiMaxKeywordLength: Int + get() = dataHandler.emojiMaxKeywordLength + set(value) { + dataHandler.emojiMaxKeywordLength = value + } + + internal var nounKeywords: HashMap> + get() = dataHandler.nounKeywords + set(value) { + dataHandler.nounKeywords = value + } + + internal var suggestionWords: HashMap> + get() = dataHandler.suggestionWords + set(value) { + dataHandler.suggestionWords = value + } + + var pluralWords: Set? + get() = dataHandler.pluralWords + set(value) { + dataHandler.pluralWords = value + } - private var emojiMaxKeywordLength: Int = 0 - internal lateinit var nounKeywords: HashMap> - internal lateinit var suggestionWords: HashMap> - var pluralWords: Set? = null - internal lateinit var caseAnnotation: HashMap> + internal var caseAnnotation: HashMap> + get() = dataHandler.caseAnnotation + set(value) { + dataHandler.caseAnnotation = value + } var emojiAutoSuggestionEnabled: Boolean = false var lastWord: String? = null @@ -258,10 +306,9 @@ abstract class GeneralKeyboardIME( */ override fun onCreate() { super.onCreate() - dbManagers = DatabaseManagers(this) + dataHandler.initialize(this) nativeSuggestionEngine = NativeSuggestionEngine(this) suggestionHandler = SuggestionHandler(this) - autocompletionManager = dbManagers.autocompletionManager autocompletionHandler = AutocompletionHandler(this) clipboardHandler.initClipboardMonitor() } @@ -667,27 +714,7 @@ abstract class GeneralKeyboardIME( } private fun loadLanguageData() { - val languageAlias = getLanguageAlias(language) - dataContract = dbManagers.getLanguageContract(languageAlias) - emojiKeywords = dbManagers.emojiManager.getEmojiKeywords(languageAlias) - emojiMaxKeywordLength = dbManagers.emojiManager.maxKeywordLength - pluralWords = - dbManagers.pluralManager - .getAllPluralForms(languageAlias, dataContract) - ?.map { it.lowercase() } - ?.toSet() - nounKeywords = dbManagers.genderManager.findGenderOfWord(languageAlias, dataContract) - suggestionWords = dbManagers.suggestionManager.getSuggestions(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") - conjugateOutput = if (tempConjugateOutput?.isEmpty() == true) null else tempConjugateOutput - conjugateLabels = dbManagers.conjugateDataManager.extractConjugateHeadings(dataContract, "coacha") + dataHandler.loadLanguageData(language) } private fun isLightColor(color: Int): Boolean { @@ -1216,15 +1243,7 @@ abstract class GeneralKeyboardIME( return nativeCompletions } } - return try { - dbManagers.autocompletionManager.getAutocompletions(prefix, limit) - } catch (e: SQLiteException) { - Log.e("GeneralKeyboardIME", "Database error in autocompletion", e) - emptyList() - } catch (e: IllegalStateException) { - Log.e("GeneralKeyboardIME", "Illegal state in autocompletion", e) - emptyList() - } + return dataHandler.getAutocompletions(prefix, limit) } /** @@ -1280,16 +1299,7 @@ abstract class GeneralKeyboardIME( * * @return The plural form as a string, or null if not found. */ - private fun getPluralRepresentation(word: String?): String? { - if (word.isNullOrEmpty()) return null - val langAlias = getLanguageAlias(language) - val lowercaseWord = word.lowercase() - if (pluralWords?.contains(lowercaseWord) == true) return ALREADY_PLURAL_MSG - return dbManagers.pluralManager - .getPluralRepresentation(langAlias, dataContract, word) - .values - .firstOrNull() - } + private fun getPluralRepresentation(word: String?): String? = dataHandler.getPluralRepresentation(language, word) /** * Retrieves the translation for a given word. @@ -1302,10 +1312,7 @@ abstract class GeneralKeyboardIME( private fun getTranslation( language: String, commandBarInput: String, - ): String { - val sourceDest = dbManagers.translationDataManager.getSourceAndDestinationLanguage(language) - return dbManagers.translationDataManager.getTranslationDataForAWord(sourceDest, commandBarInput) - } + ): String = dataHandler.getTranslation(language, commandBarInput) /** * Applies capitalization to all conjugated forms in the output map. diff --git a/app/src/testKeyboards/kotlin/be/scri/helpers/KeyboardDataHandlerTest.kt b/app/src/testKeyboards/kotlin/be/scri/helpers/KeyboardDataHandlerTest.kt new file mode 100644 index 00000000..67d03f7b --- /dev/null +++ b/app/src/testKeyboards/kotlin/be/scri/helpers/KeyboardDataHandlerTest.kt @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package be.scri.helpers + +import android.content.Context +import be.scri.helpers.data.AutocompletionDataManager +import be.scri.helpers.english.ENInterfaceVariables.ALREADY_PLURAL_MSG +import io.mockk.every +import io.mockk.mockk +import io.mockk.unmockkAll +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test + +class KeyboardDataHandlerTest { + private val context = mockk(relaxed = true) + private val autocompletionManager = mockk(relaxed = true) + private val dbManagers = mockk(relaxed = true) + private lateinit var dataHandler: KeyboardDataHandler + + @Before + fun setUp() { + dataHandler = KeyboardDataHandler() + every { dbManagers.autocompletionManager } returns autocompletionManager + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun initialState_isNotInitialized() { + assertFalse(dataHandler.isInitialized) + assertNull(dataHandler.dataContract) + assertNull(dataHandler.emojiKeywords) + assertNull(dataHandler.pluralWords) + } + + @Test + fun getPluralRepresentation_whenNullOrEmpty_returnsNull() { + assertNull(dataHandler.getPluralRepresentation("en", null)) + assertNull(dataHandler.getPluralRepresentation("en", "")) + } + + @Test + fun getPluralRepresentation_whenAlreadyPlural_returnsAlreadyPluralMsg() { + dataHandler.pluralWords = setOf("books") + assertEquals(ALREADY_PLURAL_MSG, dataHandler.getPluralRepresentation("en", "books")) + } +}