From c2cb28668d43c84cb2b4c0d2dc322fb3435a2965 Mon Sep 17 00:00:00 2001 From: Patrick Goldinger Date: Mon, 4 Jul 2022 19:30:39 +0200 Subject: [PATCH] Implement candidate auto-commit logic --- .../ime/keyboard/KeyboardManager.kt | 9 +++++ .../florisboard/ime/nlp/NlpManager.kt | 14 +++---- .../florisboard/ime/nlp/NlpProviders.kt | 18 ++++----- .../ime/nlp/latin/LatinLanguageProvider.kt | 40 +++++++++++++++---- 4 files changed, 55 insertions(+), 26 deletions(-) diff --git a/app/src/main/kotlin/dev/patrickgold/florisboard/ime/keyboard/KeyboardManager.kt b/app/src/main/kotlin/dev/patrickgold/florisboard/ime/keyboard/KeyboardManager.kt index af6a1a15..553088ba 100644 --- a/app/src/main/kotlin/dev/patrickgold/florisboard/ime/keyboard/KeyboardManager.kt +++ b/app/src/main/kotlin/dev/patrickgold/florisboard/ime/keyboard/KeyboardManager.kt @@ -17,6 +17,7 @@ package dev.patrickgold.florisboard.ime.keyboard import android.content.Context +import android.icu.lang.UCharacter import android.view.KeyEvent import androidx.compose.runtime.Composable import androidx.compose.runtime.State @@ -289,6 +290,9 @@ class KeyboardManager(context: Context) : InputKeyEventReceiver { } fun commitCandidate(candidate: SuggestionCandidate) { + scope.launch { + candidate.sourceProvider?.notifySuggestionAccepted(subtypeManager.activeSubtype, candidate) + } when (candidate) { is ClipboardSuggestionCandidate -> editorInstance.commitClipboardItem(candidate.clipboardItem) else -> editorInstance.commitCompletion(candidate.text.toString()) @@ -504,6 +508,7 @@ class KeyboardManager(context: Context) : InputKeyEventReceiver { * enabled by the user. */ private fun handleSpace(data: KeyData) { + nlpManager.getAutoCommitCandidate()?.let { commitCandidate(it) } if (prefs.keyboard.spaceBarSwitchesToCharacters.get()) { when (activeState.keyboardMode) { KeyboardMode.NUMERIC_ADVANCED, @@ -673,6 +678,7 @@ class KeyboardManager(context: Context) : InputKeyEventReceiver { KeyCode.VIEW_SYMBOLS2 -> activeState.keyboardMode = KeyboardMode.SYMBOLS2 else -> { if (activeState.imeUiMode == ImeUiMode.MEDIA) { + nlpManager.getAutoCommitCandidate()?.let { commitCandidate(it) } editorInstance.commitText(data.asString(isForDisplay = false)) return@batchEdit } @@ -697,6 +703,9 @@ class KeyboardManager(context: Context) : InputKeyEventReceiver { else -> when (data.type) { KeyType.CHARACTER, KeyType.NUMERIC ->{ val text = data.asString(isForDisplay = false) + if (!UCharacter.isUAlphabetic(UCharacter.codePointAt(text, 0))) { + nlpManager.getAutoCommitCandidate()?.let { commitCandidate(it) } + } editorInstance.commitChar(text) } else -> { diff --git a/app/src/main/kotlin/dev/patrickgold/florisboard/ime/nlp/NlpManager.kt b/app/src/main/kotlin/dev/patrickgold/florisboard/ime/nlp/NlpManager.kt index 23a282f1..e3fb2a6e 100644 --- a/app/src/main/kotlin/dev/patrickgold/florisboard/ime/nlp/NlpManager.kt +++ b/app/src/main/kotlin/dev/patrickgold/florisboard/ime/nlp/NlpManager.kt @@ -194,6 +194,10 @@ class NlpManager(context: Context) { } } + fun getAutoCommitCandidate(): SuggestionCandidate? { + return activeCandidates.firstOrNull { it.isEligibleForAutoCommit } + } + fun getListOfWords(subtype: Subtype): List { return runBlocking { getSuggestionProvider(subtype).getListOfWords(subtype) } } @@ -228,15 +232,7 @@ class NlpManager(context: Context) { } runBlocking { internalSuggestionsGuard.withLock { - internalSuggestions.let { (_, suggestions) -> - suggestions.forEachIndexed { n, candidate -> - add(WordSuggestionCandidate( - text = candidate.text, - secondaryText = if (n % 2 == 1) "secondary" else null, - confidence = 0.5, - )) - } - } + addAll(internalSuggestions.second) } } } diff --git a/app/src/main/kotlin/dev/patrickgold/florisboard/ime/nlp/NlpProviders.kt b/app/src/main/kotlin/dev/patrickgold/florisboard/ime/nlp/NlpProviders.kt index b00bcd97..81f0cea1 100644 --- a/app/src/main/kotlin/dev/patrickgold/florisboard/ime/nlp/NlpProviders.kt +++ b/app/src/main/kotlin/dev/patrickgold/florisboard/ime/nlp/NlpProviders.kt @@ -145,9 +145,9 @@ interface SuggestionProvider : NlpProvider { * * @param subtype Information about the current subtype, primarily used for getting the primary and secondary * language for correct dictionary selection. - * @param suggestion The exact suggestion candidate which has been accepted. + * @param candidate The exact suggestion candidate which has been accepted. */ - suspend fun notifySuggestionAccepted(subtype: Subtype, suggestion: SuggestionCandidate) + suspend fun notifySuggestionAccepted(subtype: Subtype, candidate: SuggestionCandidate) /** * Is called when a previously automatically accepted suggestion has been reverted by the user with backspace. This @@ -155,9 +155,9 @@ interface SuggestionProvider : NlpProvider { * * @param subtype Information about the current subtype, primarily used for getting the primary and secondary * language for correct dictionary selection. - * @param suggestion The exact suggestion candidate which has been reverted. + * @param candidate The exact suggestion candidate which has been reverted. */ - suspend fun notifySuggestionReverted(subtype: Subtype, suggestion: SuggestionCandidate) + suspend fun notifySuggestionReverted(subtype: Subtype, candidate: SuggestionCandidate) /** * Called if the user requests to prevent a certain suggested word from showing again. It is up to the actual @@ -165,11 +165,11 @@ interface SuggestionProvider : NlpProvider { * * @param subtype Information about the current subtype, primarily used for getting the primary and secondary * language for correct dictionary selection. - * @param suggestion The exact suggestion candidate which the user does not want to see again. + * @param candidate The exact suggestion candidate which the user does not want to see again. * * @return True if the removal request is supported and is accepted, false otherwise. */ - suspend fun removeSuggestion(subtype: Subtype, suggestion: SuggestionCandidate): Boolean + suspend fun removeSuggestion(subtype: Subtype, candidate: SuggestionCandidate): Boolean /** * Interop method allowing the glide typing logic to perform its own magic. @@ -231,15 +231,15 @@ object FallbackNlpProvider : SpellingProvider, SuggestionProvider { return emptyList() } - override suspend fun notifySuggestionAccepted(subtype: Subtype, suggestion: SuggestionCandidate) { + override suspend fun notifySuggestionAccepted(subtype: Subtype, candidate: SuggestionCandidate) { // Do nothing } - override suspend fun notifySuggestionReverted(subtype: Subtype, suggestion: SuggestionCandidate) { + override suspend fun notifySuggestionReverted(subtype: Subtype, candidate: SuggestionCandidate) { // Do nothing } - override suspend fun removeSuggestion(subtype: Subtype, suggestion: SuggestionCandidate): Boolean { + override suspend fun removeSuggestion(subtype: Subtype, candidate: SuggestionCandidate): Boolean { return false } diff --git a/app/src/main/kotlin/dev/patrickgold/florisboard/ime/nlp/latin/LatinLanguageProvider.kt b/app/src/main/kotlin/dev/patrickgold/florisboard/ime/nlp/latin/LatinLanguageProvider.kt index 79398112..7779c79a 100644 --- a/app/src/main/kotlin/dev/patrickgold/florisboard/ime/nlp/latin/LatinLanguageProvider.kt +++ b/app/src/main/kotlin/dev/patrickgold/florisboard/ime/nlp/latin/LatinLanguageProvider.kt @@ -26,6 +26,7 @@ import dev.patrickgold.florisboard.ime.nlp.SuggestionCandidate import dev.patrickgold.florisboard.ime.nlp.SuggestionProvider import dev.patrickgold.florisboard.ime.nlp.WordSuggestionCandidate import dev.patrickgold.florisboard.lib.android.readText +import dev.patrickgold.florisboard.lib.devtools.flogDebug import dev.patrickgold.florisboard.lib.kotlin.guardedByLock import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -48,15 +49,29 @@ class LatinLanguageProvider(context: Context) : SpellingProvider, SuggestionProv override val providerId = ProviderId override suspend fun create() { - // Here we initialize our provider, set up a potential neural network, etc. + // Here we initialize our provider, set up all things which are not language dependent. } override suspend fun preload(subtype: Subtype) = withContext(Dispatchers.IO) { // Here we have the chance to preload dictionaries and prepare a neural network for a specific language. // Is kept in sync with the active keyboard subtype of the user, however a new preload does not necessary mean // the previous language is not needed anymore (e.g. if the user constantly switches between two subtypes) + + // To read a file from the APK assets the following methods can be used: + // appContext.assets.open() + // appContext.assets.reader() + // appContext.assets.bufferedReader() + // appContext.assets.readText() + // To copy an APK file/dir to the file system cache (appContext.cacheDir), the following methods are available: + // appContext.assets.copy() + // appContext.assets.copyRecursively() + + // The subtype we get here contains a lot of data, however we are only interested in subtype.primaryLocale and + // subtype.secondaryLocales. + wordData.withLock { wordData -> if (wordData.isEmpty()) { + // Here we use readText() because the test dictionary is a json dictionary val rawData = appContext.assets.readText("ime/dict/data.json") val jsonData = Json.decodeFromString(wordDataSerializer, rawData) wordData.putAll(jsonData) @@ -74,8 +89,12 @@ class LatinLanguageProvider(context: Context) : SpellingProvider, SuggestionProv isPrivateSession: Boolean, ): SpellingResult { return when (word.lowercase()) { + // Use typo for typing errors "typo" -> SpellingResult.typo(arrayOf("typo1", "typo2", "typo3")) + // Use grammar error if the algorithm can detect this. On Android 11 and lower grammar errors are visually + // marked as typos due to a lack of support "gerror" -> SpellingResult.grammarError(arrayOf("grammar1", "grammar2", "grammar3")) + // Use valid word for valid input else -> SpellingResult.validWord() } } @@ -89,26 +108,31 @@ class LatinLanguageProvider(context: Context) : SpellingProvider, SuggestionProv ): List { val word = content.composingText.ifBlank { "next" } val suggestions = buildList { - for (n in 0..maxCandidateCount) { + for (n in 0 until maxCandidateCount) { add(WordSuggestionCandidate( text = "$word$n", secondaryText = if (n % 2 == 1) "secondary" else null, confidence = 0.5, + isEligibleForAutoCommit = n == 0 && word.startsWith("auto"), + // We set ourselves as the source provider so we can get notify events for our candidate + sourceProvider = this@LatinLanguageProvider, )) } } return suggestions } - override suspend fun notifySuggestionAccepted(subtype: Subtype, suggestion: SuggestionCandidate) { - // Ignore for now + override suspend fun notifySuggestionAccepted(subtype: Subtype, candidate: SuggestionCandidate) { + // We can use flogDebug, flogInfo, flogWarning and flogError for debug logging, which is a wrapper for Logcat + flogDebug { "notify accepted suggestion $candidate" } } - override suspend fun notifySuggestionReverted(subtype: Subtype, suggestion: SuggestionCandidate) { - // Ignore for now + override suspend fun notifySuggestionReverted(subtype: Subtype, candidate: SuggestionCandidate) { + flogDebug { "notify reverted suggestion $candidate" } } - override suspend fun removeSuggestion(subtype: Subtype, suggestion: SuggestionCandidate): Boolean { + override suspend fun removeSuggestion(subtype: Subtype, candidate: SuggestionCandidate): Boolean { + flogDebug { "remove suggestion request $candidate" } return false } @@ -122,6 +146,6 @@ class LatinLanguageProvider(context: Context) : SpellingProvider, SuggestionProv override suspend fun destroy() { // Here we have the chance to de-allocate memory and finish our work. However this might never be called if - // the app process is killed. + // the app process is killed (which will most likely always be the case). } }