diff --git a/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java b/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java index 4ac354a4d..834cc1561 100644 --- a/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java +++ b/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java @@ -95,6 +95,11 @@ public final class InputLogic { private int mSpaceState; // Never null private SuggestedWords mSuggestedWords = SuggestedWords.getEmptyInstance(); + // #14 spacing-policy signals — recomputed every keystroke from the suggestion results at zero + // extra native cost (see computeSpacingSignals / setSuggestedWords). Consumed by the upcoming + // signal-driven grace + two-gate Assisted-tier logic. + private boolean mSpacingComplete; // typed word is a real dictionary word + private float mSpacingPrefixRichScore; // fraction of candidates that are completions [0..1] private final Suggest mSuggest; private final DictionaryFacilitator mDictionaryFacilitator; private SingleDictionaryFacilitator mEmojiDictionaryFacilitator; @@ -1399,6 +1404,9 @@ public void setSuggestedWords(final SuggestedWords suggestedWords) { mWordComposer.setAutoCorrection(suggestedWordInfo); } mSuggestedWords = suggestedWords; + final SpacingSignals spacingSignals = computeSpacingSignals(suggestedWords); + mSpacingComplete = spacingSignals.complete; + mSpacingPrefixRichScore = spacingSignals.prefixRichScore; final boolean newAutoCorrectionIndicator = suggestedWords.mWillAutoCorrect; // Put a blue underline to a word in TextView which will be auto-corrected. @@ -1416,6 +1424,43 @@ public void setSuggestedWords(final SuggestedWords suggestedWords) { } } + /** + * #14 spacing-policy signals derived from the current suggestion results, computed every + * keystroke at zero extra native cost. + * + * Static + pure so it can be unit-tested without a live InputLogic. + */ + static final class SpacingSignals { + final boolean complete; + final float prefixRichScore; + SpacingSignals(final boolean complete, final float prefixRichScore) { + this.complete = complete; + this.prefixRichScore = prefixRichScore; + } + } + + static SpacingSignals computeSpacingSignals(final SuggestedWords suggestedWords) { + final int n = suggestedWords.size(); + if (n == 0) return new SpacingSignals(false, 0f); + final SuggestedWordInfo typed = suggestedWords.mTypedWordInfo; + final boolean complete = suggestedWords.mTypedWordValid + && typed != null && typed.mSourceDict != null + && !Dictionary.TYPE_USER_TYPED.equals(typed.mSourceDict.mDictType); + int completions = 0; + for (int i = 0; i < n; i++) { + if (suggestedWords.getInfo(i).getKind() == SuggestedWordInfo.KIND_COMPLETION) { + completions++; + } + } + return new SpacingSignals(complete, (float) completions / n); + } + /** * Handle a consumed event. *

diff --git a/app/src/test/java/helium314/keyboard/latin/inputlogic/SpacingSignalsTest.kt b/app/src/test/java/helium314/keyboard/latin/inputlogic/SpacingSignalsTest.kt new file mode 100644 index 000000000..e3296d37d --- /dev/null +++ b/app/src/test/java/helium314/keyboard/latin/inputlogic/SpacingSignalsTest.kt @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: GPL-3.0-only +package helium314.keyboard.latin.inputlogic + +import helium314.keyboard.latin.SuggestedWords +import helium314.keyboard.latin.SuggestedWords.SuggestedWordInfo +import helium314.keyboard.latin.dictionary.Dictionary +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +/** + * Unit tests for [InputLogic.computeSpacingSignals] (#14 spacing policy): the free per-keystroke + * `complete` + `prefixRichScore` signals derived from the suggestion results. Pure logic. + */ +class SpacingSignalsTest { + + // mDictType != "user_typed" -> counts as a "real" dictionary source for `complete`. + private val realDict: Dictionary = Dictionary.DICTIONARY_APPLICATION_DEFINED + private val userTyped: Dictionary = Dictionary.DICTIONARY_USER_TYPED + + private fun info(word: String, kind: Int, dict: Dictionary): SuggestedWordInfo = + SuggestedWordInfo(word, "", 0, kind, dict, + SuggestedWordInfo.NOT_AN_INDEX, SuggestedWordInfo.NOT_A_CONFIDENCE) + + private fun words(typed: SuggestedWordInfo?, typedValid: Boolean, + list: List): SuggestedWords = + SuggestedWords(ArrayList(list), null, typed, typedValid, false, false, + SuggestedWords.INPUT_STYLE_TYPING, SuggestedWords.NOT_A_SEQUENCE_NUMBER) + + @Test fun `empty suggestions yield no signals`() { + val s = InputLogic.computeSpacingSignals(SuggestedWords.getEmptyInstance()) + assertFalse(s.complete) + assertEquals(0f, s.prefixRichScore, 0f) + } + + @Test fun `valid typed word from a real dictionary is complete`() { + val typed = info("the", SuggestedWordInfo.KIND_TYPED, realDict) + assertTrue(InputLogic.computeSpacingSignals(words(typed, true, listOf(typed))).complete) + } + + @Test fun `valid typed word from the user-typed source is NOT complete`() { + val typed = info("xyzzy", SuggestedWordInfo.KIND_TYPED, userTyped) + assertFalse(InputLogic.computeSpacingSignals(words(typed, true, listOf(typed))).complete) + } + + @Test fun `invalid typed word is not complete`() { + val typed = info("teh", SuggestedWordInfo.KIND_TYPED, realDict) + assertFalse(InputLogic.computeSpacingSignals(words(typed, false, listOf(typed))).complete) + } + + @Test fun `prefix-rich score is the fraction of completions`() { + val typed = info("ba", SuggestedWordInfo.KIND_TYPED, realDict) + val list = listOf( + typed, + info("bad", SuggestedWordInfo.KIND_COMPLETION, realDict), + info("bat", SuggestedWordInfo.KIND_COMPLETION, realDict), + info("ball", SuggestedWordInfo.KIND_COMPLETION, realDict), + ) + // 3 completions out of 4 candidates. + assertEquals(0.75f, InputLogic.computeSpacingSignals(words(typed, false, list)).prefixRichScore, 1e-6f) + } + + @Test fun `no completions yields zero prefix-rich score`() { + val typed = info("the", SuggestedWordInfo.KIND_TYPED, realDict) + assertEquals(0f, + InputLogic.computeSpacingSignals(words(typed, true, listOf(typed))).prefixRichScore, 0f) + } +}