mirror of
https://github.com/Helium314/HeliBoard.git
synced 2025-05-18 07:53:07 +00:00
add unit tests for input logic (wip very much)
This commit is contained in:
parent
f1fde08a55
commit
70f419efe5
7 changed files with 590 additions and 270 deletions
|
@ -56,6 +56,12 @@ android {
|
||||||
useLegacyPackaging true
|
useLegacyPackaging true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testOptions {
|
||||||
|
unitTests {
|
||||||
|
includeAndroidResources = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
@ -64,5 +70,11 @@ dependencies {
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.3.1'
|
implementation 'androidx.recyclerview:recyclerview:1.3.1'
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||||
implementation 'com.github.skydoves:colorpickerview:2.2.4'
|
implementation 'com.github.skydoves:colorpickerview:2.2.4'
|
||||||
|
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
|
testImplementation 'org.mockito:mockito-core:3.12.4'
|
||||||
|
testImplementation 'org.mockito:mockito-inline:3.12.4'
|
||||||
|
testImplementation 'org.robolectric:robolectric:4.9'
|
||||||
|
testImplementation 'androidx.test:runner:1.5.2'
|
||||||
|
testImplementation 'androidx.test:core:1.4.0'
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,8 @@ private class CrashReportExceptionHandler(val appContext: Context) : Thread.Unca
|
||||||
|
|
||||||
fun install(): Boolean {
|
fun install(): Boolean {
|
||||||
val ueh = Thread.getDefaultUncaughtExceptionHandler()
|
val ueh = Thread.getDefaultUncaughtExceptionHandler()
|
||||||
check(ueh !is CrashReportExceptionHandler) { "May not install several CrashReportExceptionHandlers!" }
|
if (ueh is CrashReportExceptionHandler)
|
||||||
|
return false
|
||||||
defaultUncaughtExceptionHandler = ueh
|
defaultUncaughtExceptionHandler = ueh
|
||||||
Thread.setDefaultUncaughtExceptionHandler(this)
|
Thread.setDefaultUncaughtExceptionHandler(this)
|
||||||
return true
|
return true
|
||||||
|
|
|
@ -18,6 +18,7 @@ package org.dslul.openboard.inputmethod.latin;
|
||||||
|
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import org.dslul.openboard.inputmethod.annotations.UsedForTesting;
|
||||||
import org.dslul.openboard.inputmethod.keyboard.Keyboard;
|
import org.dslul.openboard.inputmethod.keyboard.Keyboard;
|
||||||
import org.dslul.openboard.inputmethod.keyboard.KeyboardId;
|
import org.dslul.openboard.inputmethod.keyboard.KeyboardId;
|
||||||
import org.dslul.openboard.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
|
import org.dslul.openboard.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
|
||||||
|
@ -203,8 +204,6 @@ public final class Suggest {
|
||||||
keyboard.mId.mMode,
|
keyboard.mId.mMode,
|
||||||
wordComposer,
|
wordComposer,
|
||||||
suggestionResults,
|
suggestionResults,
|
||||||
mDictionaryFacilitator,
|
|
||||||
mAutoCorrectionThreshold,
|
|
||||||
firstOccurrenceOfTypedWordInSuggestions,
|
firstOccurrenceOfTypedWordInSuggestions,
|
||||||
typedWordFirstOccurrenceWordInfo
|
typedWordFirstOccurrenceWordInfo
|
||||||
);
|
);
|
||||||
|
@ -282,7 +281,8 @@ public final class Suggest {
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns [allowsToBeAutoCorrected, hasAutoCorrection]
|
// returns [allowsToBeAutoCorrected, hasAutoCorrection]
|
||||||
static boolean[] shouldBeAutoCorrected(
|
@UsedForTesting
|
||||||
|
boolean[] shouldBeAutoCorrected(
|
||||||
final int trailingSingleQuotesCount,
|
final int trailingSingleQuotesCount,
|
||||||
final String typedWordString,
|
final String typedWordString,
|
||||||
final List<SuggestedWordInfo> suggestionsContainer,
|
final List<SuggestedWordInfo> suggestionsContainer,
|
||||||
|
@ -293,8 +293,6 @@ public final class Suggest {
|
||||||
final int keyboardIdMode,
|
final int keyboardIdMode,
|
||||||
final WordComposer wordComposer,
|
final WordComposer wordComposer,
|
||||||
final SuggestionResults suggestionResults,
|
final SuggestionResults suggestionResults,
|
||||||
final DictionaryFacilitator dictionaryFacilitator,
|
|
||||||
final float autoCorrectionThreshold,
|
|
||||||
final int firstOccurrenceOfTypedWordInSuggestions,
|
final int firstOccurrenceOfTypedWordInSuggestions,
|
||||||
final SuggestedWordInfo typedWordFirstOccurrenceWordInfo
|
final SuggestedWordInfo typedWordFirstOccurrenceWordInfo
|
||||||
) {
|
) {
|
||||||
|
@ -365,7 +363,7 @@ public final class Suggest {
|
||||||
// list, "will" would always auto-correct to "Will" which is unwanted. Hence, no
|
// list, "will" would always auto-correct to "Will" which is unwanted. Hence, no
|
||||||
// main dict => no auto-correct. Also, it would probably get obnoxious quickly.
|
// main dict => no auto-correct. Also, it would probably get obnoxious quickly.
|
||||||
// TODO: now that we have personalization, we may want to re-evaluate this decision
|
// TODO: now that we have personalization, we may want to re-evaluate this decision
|
||||||
|| !dictionaryFacilitator.hasAtLeastOneInitializedMainDictionary()) {
|
|| !mDictionaryFacilitator.hasAtLeastOneInitializedMainDictionary()) {
|
||||||
hasAutoCorrection = false;
|
hasAutoCorrection = false;
|
||||||
} else {
|
} else {
|
||||||
final SuggestedWordInfo firstSuggestion = suggestionResults.first();
|
final SuggestedWordInfo firstSuggestion = suggestionResults.first();
|
||||||
|
@ -376,7 +374,7 @@ public final class Suggest {
|
||||||
return new boolean[]{ true, true };
|
return new boolean[]{ true, true };
|
||||||
}
|
}
|
||||||
if (!AutoCorrectionUtils.suggestionExceedsThreshold(
|
if (!AutoCorrectionUtils.suggestionExceedsThreshold(
|
||||||
firstSuggestion, consideredWord, autoCorrectionThreshold)) {
|
firstSuggestion, consideredWord, mAutoCorrectionThreshold)) {
|
||||||
// todo: maybe also do something here depending on ngram context?
|
// todo: maybe also do something here depending on ngram context?
|
||||||
// Score is too low for autocorrect
|
// Score is too low for autocorrect
|
||||||
return new boolean[]{ true, false };
|
return new boolean[]{ true, false };
|
||||||
|
@ -390,7 +388,7 @@ public final class Suggest {
|
||||||
// typed word is valid and has good score
|
// typed word is valid and has good score
|
||||||
// do not auto-correct if typed word is better match than first suggestion
|
// do not auto-correct if typed word is better match than first suggestion
|
||||||
final SuggestedWordInfo first = firstSuggestionInContainer != null ? firstSuggestionInContainer : firstSuggestion;
|
final SuggestedWordInfo first = firstSuggestionInContainer != null ? firstSuggestionInContainer : firstSuggestion;
|
||||||
final Locale dictLocale = dictionaryFacilitator.getCurrentLocale();
|
final Locale dictLocale = mDictionaryFacilitator.getCurrentLocale();
|
||||||
|
|
||||||
if (first.mScore < scoreLimit) {
|
if (first.mScore < scoreLimit) {
|
||||||
// don't allow if suggestion has too low score
|
// don't allow if suggestion has too low score
|
||||||
|
|
|
@ -16,18 +16,11 @@
|
||||||
|
|
||||||
package org.dslul.openboard.inputmethod.latin.utils;
|
package org.dslul.openboard.inputmethod.latin.utils;
|
||||||
|
|
||||||
import static android.view.KeyEvent.KEYCODE_SPACE;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import com.android.inputmethod.latin.utils.BinaryDictionaryUtils;
|
import com.android.inputmethod.latin.utils.BinaryDictionaryUtils;
|
||||||
|
|
||||||
import org.dslul.openboard.inputmethod.annotations.UsedForTesting;
|
|
||||||
import org.dslul.openboard.inputmethod.latin.BuildConfig;
|
|
||||||
import org.dslul.openboard.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
|
import org.dslul.openboard.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
|
||||||
import org.dslul.openboard.inputmethod.latin.common.StringUtils;
|
|
||||||
import org.dslul.openboard.inputmethod.latin.define.DebugFlags;
|
import org.dslul.openboard.inputmethod.latin.define.DebugFlags;
|
||||||
|
|
||||||
public final class AutoCorrectionUtils {
|
public final class AutoCorrectionUtils {
|
||||||
|
@ -38,7 +31,6 @@ public final class AutoCorrectionUtils {
|
||||||
// Purely static class: can't instantiate.
|
// Purely static class: can't instantiate.
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ObsoleteSdkInt") // SDK_INT is 0 in unit tests
|
|
||||||
public static boolean suggestionExceedsThreshold(final SuggestedWordInfo suggestion,
|
public static boolean suggestionExceedsThreshold(final SuggestedWordInfo suggestion,
|
||||||
final String consideredWord, final float threshold) {
|
final String consideredWord, final float threshold) {
|
||||||
if (null != suggestion) {
|
if (null != suggestion) {
|
||||||
|
@ -53,13 +45,7 @@ public final class AutoCorrectionUtils {
|
||||||
final int autoCorrectionSuggestionScore = suggestion.mScore;
|
final int autoCorrectionSuggestionScore = suggestion.mScore;
|
||||||
// TODO: when the normalized score of the first suggestion is nearly equals to
|
// TODO: when the normalized score of the first suggestion is nearly equals to
|
||||||
// the normalized score of the second suggestion, behave less aggressive.
|
// the normalized score of the second suggestion, behave less aggressive.
|
||||||
final float normalizedScore;
|
final float normalizedScore = BinaryDictionaryUtils.calcNormalizedScore(
|
||||||
if (BuildConfig.DEBUG && Build.VERSION.SDK_INT == 0)
|
|
||||||
normalizedScore = calcNormalizedScore(StringUtils.toCodePointArray(consideredWord),
|
|
||||||
StringUtils.toCodePointArray(suggestion.mWord), autoCorrectionSuggestionScore,
|
|
||||||
editDistance(consideredWord, suggestion.mWord));
|
|
||||||
else
|
|
||||||
normalizedScore = BinaryDictionaryUtils.calcNormalizedScore(
|
|
||||||
consideredWord, suggestion.mWord, autoCorrectionSuggestionScore);
|
consideredWord, suggestion.mWord, autoCorrectionSuggestionScore);
|
||||||
if (DBG) {
|
if (DBG) {
|
||||||
Log.d(TAG, "Normalized " + consideredWord + "," + suggestion + ","
|
Log.d(TAG, "Normalized " + consideredWord + "," + suggestion + ","
|
||||||
|
@ -75,72 +61,4 @@ public final class AutoCorrectionUtils {
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// below is normalized score calculation in java, to allow unit tests involving suggestionExceedsThreshold
|
|
||||||
@UsedForTesting
|
|
||||||
private static float calcNormalizedScore(final int[] before,
|
|
||||||
final int[] after, final int score, final int distance) {
|
|
||||||
final int beforeLength = before.length;
|
|
||||||
final int afterLength = after.length;
|
|
||||||
if (0 == beforeLength || 0 == afterLength)
|
|
||||||
return 0.0f;
|
|
||||||
|
|
||||||
int spaceCount = 0;
|
|
||||||
for (int j : after) {
|
|
||||||
if (j == KEYCODE_SPACE)
|
|
||||||
++spaceCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (spaceCount == afterLength)
|
|
||||||
return 0.0f;
|
|
||||||
|
|
||||||
if (score <= 0 || distance >= afterLength) {
|
|
||||||
// normalizedScore must be 0.0f (the minimum value) if the score is less than or equal to 0,
|
|
||||||
// or if the edit distance is larger than or equal to afterLength.
|
|
||||||
return 0.0f;
|
|
||||||
}
|
|
||||||
// add a weight based on edit distance.
|
|
||||||
final float weight = 1.0f - (float) distance / (float) afterLength;
|
|
||||||
|
|
||||||
return ((float) score / 1000000.0f) * weight;
|
|
||||||
}
|
|
||||||
|
|
||||||
@UsedForTesting
|
|
||||||
private static int editDistance(String x, String y) {
|
|
||||||
int[][] dp = new int[x.length() + 1][y.length() + 1];
|
|
||||||
|
|
||||||
for (int i = 0; i <= x.length(); i++) {
|
|
||||||
for (int j = 0; j <= y.length(); j++) {
|
|
||||||
if (i == 0) {
|
|
||||||
dp[i][j] = j;
|
|
||||||
}
|
|
||||||
else if (j == 0) {
|
|
||||||
dp[i][j] = i;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
dp[i][j] = min(dp[i - 1][j - 1]
|
|
||||||
+ costOfSubstitution(x.charAt(i - 1), y.charAt(j - 1)),
|
|
||||||
dp[i - 1][j] + 1,
|
|
||||||
dp[i][j - 1] + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return dp[x.length()][y.length()];
|
|
||||||
}
|
|
||||||
|
|
||||||
@UsedForTesting
|
|
||||||
private static int min(int... numbers) {
|
|
||||||
int min = Integer.MAX_VALUE;
|
|
||||||
for (int n : numbers) {
|
|
||||||
if (n < min)
|
|
||||||
min = n;
|
|
||||||
}
|
|
||||||
return min;
|
|
||||||
}
|
|
||||||
|
|
||||||
@UsedForTesting
|
|
||||||
private static int costOfSubstitution(char a, char b) {
|
|
||||||
return a == b ? 0 : 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,375 @@
|
||||||
|
package org.dslul.openboard.inputmethod.latin
|
||||||
|
|
||||||
|
import android.inputmethodservice.InputMethodService
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.text.InputType
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import android.view.inputmethod.*
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import androidx.test.runner.AndroidJUnit4
|
||||||
|
import org.dslul.openboard.inputmethod.event.Event
|
||||||
|
import org.dslul.openboard.inputmethod.keyboard.KeyboardSwitcher
|
||||||
|
import org.dslul.openboard.inputmethod.keyboard.MainKeyboardView
|
||||||
|
import org.dslul.openboard.inputmethod.latin.common.StringUtils
|
||||||
|
import org.dslul.openboard.inputmethod.latin.inputlogic.InputLogic
|
||||||
|
import org.dslul.openboard.inputmethod.latin.settings.Settings
|
||||||
|
import org.dslul.openboard.inputmethod.latin.utils.DeviceProtectedUtils
|
||||||
|
import org.dslul.openboard.inputmethod.latin.utils.ScriptUtils
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.mockito.Mockito
|
||||||
|
import org.robolectric.Robolectric
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
import org.robolectric.annotation.Implementation
|
||||||
|
import org.robolectric.annotation.Implements
|
||||||
|
import org.robolectric.shadows.ShadowLog
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
@Config(shadows = [
|
||||||
|
ShadowLocaleManagerCompat::class,
|
||||||
|
ShadowInputMethodManager2::class,
|
||||||
|
ShadowInputMethodService::class,
|
||||||
|
ShadowKeyboardSwitcher::class,
|
||||||
|
])
|
||||||
|
class InputLogicTest {
|
||||||
|
private lateinit var latinIME: LatinIME
|
||||||
|
// todo: test whether settings changes actually reloads and affects settingsValues
|
||||||
|
private val settingsValues get() = Settings.getInstance().current
|
||||||
|
private val inputLogic get() = latinIME.mInputLogic
|
||||||
|
private val connection: RichInputConnection get() = inputLogic.mConnection
|
||||||
|
private val composerReader = InputLogic::class.java.getDeclaredField("mWordComposer").apply { isAccessible = true }
|
||||||
|
private val composer get() = composerReader.get(inputLogic) as WordComposer
|
||||||
|
private val beforeComposingReader = RichInputConnection::class.java.getDeclaredField("mCommittedTextBeforeComposingText").apply { isAccessible = true }
|
||||||
|
val connectionTextBeforeComposingText get() = beforeComposingReader.get(connection) as CharSequence
|
||||||
|
private val composingReader = RichInputConnection::class.java.getDeclaredField("mComposingText").apply { isAccessible = true }
|
||||||
|
val connectionComposingText get() = composingReader.get(connection) as CharSequence
|
||||||
|
|
||||||
|
@Before fun setUp() {
|
||||||
|
latinIME = Robolectric.setupService(LatinIME::class.java)
|
||||||
|
// start logging only after latinIME is created, avoids showing the stack traces if library is not found
|
||||||
|
ShadowLog.setupLogging()
|
||||||
|
ShadowLog.stream = System.out
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun inputCode() {
|
||||||
|
reset()
|
||||||
|
input('c'.code)
|
||||||
|
assertEquals("c", connection.getTextBeforeCursor(10, 0).toString())
|
||||||
|
assertEquals("c", composingText)
|
||||||
|
latinIME.mHandler.onFinishInput()
|
||||||
|
assertEquals("", composingText)
|
||||||
|
println(inputLogic.mLastComposedWord.mTypedWord) // what do we expect here? currently it's empty
|
||||||
|
// connection.finishComposingText()
|
||||||
|
assertEquals("c", textBeforeCursor)
|
||||||
|
assertEquals("c", getText())
|
||||||
|
assertEquals("", textAfterCursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun insertLetterInWord() {
|
||||||
|
reset()
|
||||||
|
setText("hello")
|
||||||
|
println("typed1 "+composer.typedWord) // todo: should this be empty? check in actual app, also how it gets set
|
||||||
|
setCursorPosition(3) // after first l
|
||||||
|
println("typed2 "+composer.typedWord)
|
||||||
|
input('i'.code)
|
||||||
|
println("typed3 "+composer.typedWord)
|
||||||
|
assertEquals("helilo", getWordAtCursor())
|
||||||
|
assertEquals("helilo", getText())
|
||||||
|
assertEquals("helilo", textBeforeCursor + textAfterCursor)
|
||||||
|
assertEquals(4, getCursorPosition())
|
||||||
|
assertEquals(4, cursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo: why is this working here, but broken on phone?
|
||||||
|
// because the typed word is empty -> how to fix?
|
||||||
|
// this is always the case, and REALLY bad for correct mocking...
|
||||||
|
// also todo: setComposingRegion replaces composing text, that may be the (old) issue of deleting text when pressing space
|
||||||
|
@Test fun insertHangulLetterInWord() {
|
||||||
|
reset()
|
||||||
|
currentScript = ScriptUtils.SCRIPT_HANGUL
|
||||||
|
setText("ㅛㅎㄹㅎㅕㅛ")
|
||||||
|
setCursorPosition(3) // after first l
|
||||||
|
input('ㄲ'.code)
|
||||||
|
assertEquals("ㅛㅎㄹㄲㅎㅕㅛ", getWordAtCursor())
|
||||||
|
assertEquals("ㅛㅎㄹㄲㅎㅕㅛ", getText())
|
||||||
|
assertEquals("ㅛㅎㄹㄲㅎㅕㅛ", textBeforeCursor + textAfterCursor)
|
||||||
|
assertEquals(4, getCursorPosition())
|
||||||
|
assertEquals(4, cursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun setAutospace() {
|
||||||
|
println(settingsValues.mAutospaceAfterPunctuationEnabled)
|
||||||
|
setAutospaceAfterPunctuation(true)
|
||||||
|
println(settingsValues.mAutospaceAfterPunctuationEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------- helper functions ---------
|
||||||
|
|
||||||
|
// should be called before every test, so the same state is guaranteed
|
||||||
|
private fun reset() {
|
||||||
|
// reset input connection
|
||||||
|
currentScript = ScriptUtils.SCRIPT_LATIN
|
||||||
|
text = ""
|
||||||
|
selectionStart = 0
|
||||||
|
selectionEnd = 0
|
||||||
|
composingStart = 0
|
||||||
|
composingEnd = 0
|
||||||
|
batchEdit = 0
|
||||||
|
|
||||||
|
// reset settings
|
||||||
|
DeviceProtectedUtils.getSharedPreferences(latinIME).edit { clear() }
|
||||||
|
|
||||||
|
// todo: does it work if i to setText("") instead?
|
||||||
|
// plus restarting = true maybe?
|
||||||
|
// that would be the better method for setting a new text field
|
||||||
|
connection.setSelection(0, 0) // resets cache
|
||||||
|
inputLogic.restartSuggestionsOnWordTouchedByCursor(settingsValues, currentScript)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun input(codePoint: Int) {
|
||||||
|
val oldBefore = textBeforeCursor
|
||||||
|
val oldAfter = textAfterCursor
|
||||||
|
val insert = StringUtils.newSingleCodePointString(codePoint)
|
||||||
|
// essentially we replace the selected text in the input connection
|
||||||
|
// todo: what about composing text?
|
||||||
|
text = textBeforeCursor + insert + textAfterCursor
|
||||||
|
selectionStart += insert.length
|
||||||
|
selectionEnd = selectionStart
|
||||||
|
|
||||||
|
latinIME.onEvent(Event.createEventForCodePointFromUnknownSource(codePoint))
|
||||||
|
assertEquals(oldBefore + insert, textBeforeCursor)
|
||||||
|
assertEquals(oldAfter, textAfterCursor)
|
||||||
|
assertEquals(textBeforeCursor + textAfterCursor, getText())
|
||||||
|
checkConnectionConsistency()
|
||||||
|
}
|
||||||
|
|
||||||
|
// almost the same as codePoint input, but calls different latinIME function
|
||||||
|
private fun input(insert: String) {
|
||||||
|
val oldBefore = textBeforeCursor
|
||||||
|
val oldAfter = textAfterCursor
|
||||||
|
// essentially we replace the selected text in the input connection
|
||||||
|
// todo: what about composing text?
|
||||||
|
text = textBeforeCursor + insert + textAfterCursor
|
||||||
|
selectionStart += insert.length
|
||||||
|
selectionEnd = selectionStart
|
||||||
|
latinIME.onTextInput(text)
|
||||||
|
assertEquals(oldBefore + insert, textBeforeCursor)
|
||||||
|
assertEquals(oldAfter, textAfterCursor)
|
||||||
|
assertEquals(textBeforeCursor + textAfterCursor, getText())
|
||||||
|
checkConnectionConsistency()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getWordAtCursor() = connection.getWordRangeAtCursor(
|
||||||
|
settingsValues.mSpacingAndPunctuations,
|
||||||
|
currentScript,
|
||||||
|
false
|
||||||
|
).mWord
|
||||||
|
|
||||||
|
private fun getUnderlinedWord() =
|
||||||
|
getText().substring(inputLogic.composingStart, inputLogic.composingStart + inputLogic.composingLength)
|
||||||
|
|
||||||
|
private fun setCursorPosition(start: Int, end: Int = start) {
|
||||||
|
val ei = EditorInfo()
|
||||||
|
ei.inputType = 180225 // what's this? some multi-line thing...
|
||||||
|
//ei.inputType = InputType.TYPE_CLASS_TEXT // blabla caps mode and stuff
|
||||||
|
ei.initialSelStart = start
|
||||||
|
ei.initialSelEnd = end
|
||||||
|
// imeOptions should not matter
|
||||||
|
|
||||||
|
// adjust text in inputConnection first, otherwise fixLyingCursorPosition will move cursor
|
||||||
|
// to the end of the text
|
||||||
|
val fullText = textBeforeCursor + textAfterCursor
|
||||||
|
assertEquals(fullText, getText())
|
||||||
|
selectionStart = start
|
||||||
|
selectionEnd = end
|
||||||
|
assertEquals(fullText, textBeforeCursor + textAfterCursor)
|
||||||
|
// todo: any effect on composing span? check what happens
|
||||||
|
|
||||||
|
// hmm, when restarting should be true?
|
||||||
|
// todo: check! probably when switching text fields
|
||||||
|
latinIME.mHandler.onStartInput(ei, false) // essentially does nothing
|
||||||
|
latinIME.mHandler.onStartInputView(ei, false) // does the thing
|
||||||
|
assertEquals(fullText, getText()) // this may only be correct after start input?
|
||||||
|
checkConnectionConsistency()
|
||||||
|
}
|
||||||
|
|
||||||
|
// assumes we have nothing selected
|
||||||
|
private fun getCursorPosition(): Int {
|
||||||
|
assertEquals(cursor, connection.expectedSelectionStart)
|
||||||
|
assertEquals(cursor, connection.expectedSelectionEnd)
|
||||||
|
return cursor
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setText(newText: String) {
|
||||||
|
text = newText
|
||||||
|
selectionStart = newText.length
|
||||||
|
selectionEnd = selectionStart
|
||||||
|
composingStart = -1
|
||||||
|
composingStart = -1
|
||||||
|
|
||||||
|
// we need to start input to notify that something changed
|
||||||
|
latinIME.mHandler.onStartInput(EditorInfo(), false)
|
||||||
|
latinIME.mHandler.onStartInputView(EditorInfo(), false)
|
||||||
|
checkConnectionConsistency()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkConnectionConsistency() {
|
||||||
|
assertEquals(selectionStart, connection.expectedSelectionStart)
|
||||||
|
assertEquals(selectionEnd, connection.expectedSelectionEnd)
|
||||||
|
assertEquals(textBeforeComposingText, connectionTextBeforeComposingText)
|
||||||
|
assertEquals(composingText, connectionComposingText)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getText() =
|
||||||
|
connection.getTextBeforeCursor(100, 0).toString() + connection.getTextAfterCursor(100, 0)
|
||||||
|
|
||||||
|
// nice, this really reloads the prefs!!
|
||||||
|
private fun setAutospaceAfterPunctuation(enabled: Boolean) {
|
||||||
|
DeviceProtectedUtils.getSharedPreferences(latinIME)
|
||||||
|
.edit { putBoolean(Settings.PREF_AUTOSPACE_AFTER_PUNCTUATION, enabled) }
|
||||||
|
assertEquals(enabled, settingsValues.mAutospaceAfterPunctuationEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private var currentInputType = InputType.TYPE_CLASS_TEXT
|
||||||
|
private var currentScript = ScriptUtils.SCRIPT_LATIN
|
||||||
|
// inputconnection stuff
|
||||||
|
private var batchEdit = 0
|
||||||
|
private var text = ""
|
||||||
|
private var selectionStart = 0
|
||||||
|
private var selectionEnd = 0
|
||||||
|
private var composingStart = -1
|
||||||
|
private var composingEnd = -1
|
||||||
|
// convenience for access
|
||||||
|
private val textBeforeCursor get() = text.substring(0, selectionStart)
|
||||||
|
private val textAfterCursor get() = text.substring(selectionEnd)
|
||||||
|
private val textBeforeComposingText get() = if (composingStart == -1) "" else text.substring(composingStart)
|
||||||
|
private val composingText get() = if (composingStart == -1 || composingEnd == -1) ""
|
||||||
|
else text.substring(composingStart, composingEnd)
|
||||||
|
private val cursor get() = if (selectionStart == selectionEnd) selectionStart else -1
|
||||||
|
|
||||||
|
// essentially this is the text field we're editing in
|
||||||
|
private val ic = object : InputConnection {
|
||||||
|
// pretty clear (though this may be slow depending on the editor)
|
||||||
|
// bad return value here is likely the cause for that weird bug improved/fixed by fixIncorrectLength
|
||||||
|
override fun getTextBeforeCursor(p0: Int, p1: Int): CharSequence = textBeforeCursor.take(p0)
|
||||||
|
// pretty clear (though this may be slow depending on the editor)
|
||||||
|
override fun getTextAfterCursor(p0: Int, p1: Int): CharSequence = textAfterCursor.take(p0)
|
||||||
|
// pretty clear
|
||||||
|
override fun getSelectedText(p0: Int): CharSequence? = if (selectionStart == selectionEnd) null
|
||||||
|
else text.substring(selectionStart, selectionEnd)
|
||||||
|
// inserts text at cursor (right?), and sets it as composing text
|
||||||
|
// this REPLACES currently composing text (even if at a different position)
|
||||||
|
// moves the cursor: positive means relative to composing text, negative means relative to start
|
||||||
|
override fun setComposingText(newText: CharSequence, cursor: Int): Boolean {
|
||||||
|
// first remove the composing text if necessary
|
||||||
|
if (composingStart != -1 && composingEnd != -1)
|
||||||
|
text = textBeforeComposingText + text.substring(composingEnd)
|
||||||
|
else // no composing span active, we should remove selected text
|
||||||
|
if (selectionStart != selectionEnd) {
|
||||||
|
text = textBeforeCursor + textAfterCursor
|
||||||
|
selectionEnd = selectionStart
|
||||||
|
}
|
||||||
|
// then set the new text at old composing start
|
||||||
|
// if no composing start, set it at cursor position
|
||||||
|
val insertStart = if (composingStart == -1) selectionStart else composingStart
|
||||||
|
text = text.substring(0, insertStart) + newText + text.substring(insertStart)
|
||||||
|
composingStart = insertStart
|
||||||
|
composingEnd = insertStart + newText.length
|
||||||
|
selectionStart = if (insertStart > 0) insertStart + cursor
|
||||||
|
else cursor
|
||||||
|
selectionEnd = selectionStart
|
||||||
|
// todo: this should call InputMethodManager#updateSelection(View, int, int, int, int)
|
||||||
|
// but only after batch edit has ended
|
||||||
|
// this is not used in RichInputMethodManager, but probably ends up in LatinIME.onUpdateSelection
|
||||||
|
// -> DO IT (though it will likely only trigger that belatedSelectionUpdate thing, it might be relevant)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// sets composing text empty, but doesn't change actual text
|
||||||
|
override fun finishComposingText(): Boolean {
|
||||||
|
composingStart = -1
|
||||||
|
composingEnd = -1
|
||||||
|
return true // always true
|
||||||
|
}
|
||||||
|
// as per documentation: "This behaves like calling setComposingText(text, newCursorPosition) then finishComposingText()"
|
||||||
|
override fun commitText(p0: CharSequence, p1: Int): Boolean {
|
||||||
|
setComposingText(p0, p1)
|
||||||
|
finishComposingText()
|
||||||
|
return true // whether we added the text
|
||||||
|
}
|
||||||
|
// just tells the text field that we add many updated, and that the editor should not
|
||||||
|
// send status updates until batch edit ended (not actually used for this simulation)
|
||||||
|
override fun beginBatchEdit(): Boolean {
|
||||||
|
++batchEdit
|
||||||
|
return true // always true
|
||||||
|
}
|
||||||
|
// end a batch edit, but maybe there are multiple batch edits happening
|
||||||
|
override fun endBatchEdit(): Boolean {
|
||||||
|
if (batchEdit > 0)
|
||||||
|
return --batchEdit == 0
|
||||||
|
return false // returns true if there is still a batch edit ongoing
|
||||||
|
}
|
||||||
|
// should notify about cursor info containing composing text, selection, ...
|
||||||
|
// todo: maybe that could be interesting, implement it?
|
||||||
|
override fun requestCursorUpdates(p0: Int): Boolean {
|
||||||
|
// we call this, but don't have onUpdateCursorAnchorInfo overridden in latinIME, so it does nothing
|
||||||
|
// also currently we don't care about the return value
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
override fun getCursorCapsMode(p0: Int): Int = TODO("Not yet implemented")
|
||||||
|
override fun getExtractedText(p0: ExtractedTextRequest?, p1: Int): ExtractedText = TODO("Not yet implemented")
|
||||||
|
override fun deleteSurroundingText(p0: Int, p1: Int): Boolean = TODO("Not yet implemented")
|
||||||
|
override fun deleteSurroundingTextInCodePoints(p0: Int, p1: Int): Boolean = TODO("Not yet implemented")
|
||||||
|
override fun setComposingRegion(p0: Int, p1: Int): Boolean = TODO("Not yet implemented")
|
||||||
|
override fun commitCompletion(p0: CompletionInfo?): Boolean = TODO("Not yet implemented")
|
||||||
|
override fun commitCorrection(p0: CorrectionInfo?): Boolean = TODO("Not yet implemented")
|
||||||
|
override fun setSelection(p0: Int, p1: Int): Boolean = TODO("Not yet implemented")
|
||||||
|
override fun performEditorAction(p0: Int): Boolean = TODO("Not yet implemented")
|
||||||
|
override fun performContextMenuAction(p0: Int): Boolean = TODO("Not yet implemented")
|
||||||
|
override fun sendKeyEvent(p0: KeyEvent?): Boolean = TODO("Not yet implemented")
|
||||||
|
override fun clearMetaKeyStates(p0: Int): Boolean = TODO("Not yet implemented")
|
||||||
|
override fun reportFullscreenMode(p0: Boolean): Boolean = TODO("Not yet implemented")
|
||||||
|
override fun performPrivateCommand(p0: String?, p1: Bundle?): Boolean = TODO("Not yet implemented")
|
||||||
|
override fun getHandler(): Handler? = TODO("Not yet implemented")
|
||||||
|
override fun closeConnection() = TODO("Not yet implemented")
|
||||||
|
override fun commitContent(p0: InputContentInfo, p1: Int, p2: Bundle?): Boolean = TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shadows are handled by Robolectric. @Implementation overrides built-in functionality.
|
||||||
|
// This is used for avoiding crashes (LocaleManagerCompat, InputMethodManager, KeyboardSwitcher)
|
||||||
|
// and for simulating system stuff (InputMethodService for controlling the InputConnection, which
|
||||||
|
// more or less is the contents of the text field), and for setting the current script in
|
||||||
|
// KeyboardSwitcher without having to care about InputMethodSubtypes
|
||||||
|
|
||||||
|
// could also extend LatinIME, it's not final anyway
|
||||||
|
@Implements(InputMethodService::class)
|
||||||
|
class ShadowInputMethodService {
|
||||||
|
@Implementation
|
||||||
|
fun getCurrentInputEditorInfo() = EditorInfo().apply {
|
||||||
|
inputType = currentInputType
|
||||||
|
// anything else?
|
||||||
|
}
|
||||||
|
@Implementation
|
||||||
|
fun getCurrentInputConnection() = ic
|
||||||
|
}
|
||||||
|
|
||||||
|
@Implements(KeyboardSwitcher::class)
|
||||||
|
class ShadowKeyboardSwitcher {
|
||||||
|
@Implementation
|
||||||
|
// basically only needed for null check
|
||||||
|
fun getMainKeyboardView(): MainKeyboardView = Mockito.mock(MainKeyboardView::class.java)
|
||||||
|
@Implementation
|
||||||
|
// only affects view
|
||||||
|
fun setKeyboard(keyboardId: Int, toggleState: KeyboardSwitcher.KeyboardSwitchState) = Unit
|
||||||
|
@Implementation
|
||||||
|
// only affects view
|
||||||
|
fun setOneHandedModeEnabled(enabled: Boolean) = Unit
|
||||||
|
@Implementation
|
||||||
|
// only affects view
|
||||||
|
fun getCurrentKeyboardScriptId() = currentScript
|
||||||
|
}
|
|
@ -0,0 +1,98 @@
|
||||||
|
package org.dslul.openboard.inputmethod.latin
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import android.view.inputmethod.InputMethodInfo
|
||||||
|
import android.view.inputmethod.InputMethodManager
|
||||||
|
import android.view.inputmethod.InputMethodSubtype
|
||||||
|
import androidx.core.app.LocaleManagerCompat
|
||||||
|
import androidx.core.os.LocaleListCompat
|
||||||
|
import com.android.inputmethod.latin.utils.BinaryDictionaryUtils
|
||||||
|
import org.dslul.openboard.inputmethod.latin.common.StringUtils
|
||||||
|
import org.robolectric.annotation.Implementation
|
||||||
|
import org.robolectric.annotation.Implements
|
||||||
|
import org.robolectric.shadows.ShadowInputMethodManager
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
@Implements(LocaleManagerCompat::class)
|
||||||
|
object ShadowLocaleManagerCompat {
|
||||||
|
@Implementation
|
||||||
|
@JvmStatic
|
||||||
|
fun getSystemLocales(context: Context) = LocaleListCompat.create(Locale.ENGLISH)
|
||||||
|
}
|
||||||
|
|
||||||
|
// why doesn't the original ShadowInputMethodManager simply work?
|
||||||
|
@Implements(InputMethodManager::class)
|
||||||
|
class ShadowInputMethodManager2 : ShadowInputMethodManager() {
|
||||||
|
@Implementation
|
||||||
|
override fun getInputMethodList() = listOf(
|
||||||
|
InputMethodInfo("org.dslul.openboard.inputmethod.latin.debug", "LatinIME", "openboard debug", null),
|
||||||
|
)
|
||||||
|
@Implementation
|
||||||
|
fun getShortcutInputMethodsAndSubtypes() = emptyMap<InputMethodInfo, List<InputMethodSubtype>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Implements(BinaryDictionaryUtils::class)
|
||||||
|
object ShadowBinaryDictionaryUtils {
|
||||||
|
@Implementation
|
||||||
|
@JvmStatic
|
||||||
|
fun calcNormalizedScore(beforeString: String, afterString: String, score: Int): Float {
|
||||||
|
val before = StringUtils.toCodePointArray(beforeString)
|
||||||
|
val after = StringUtils.toCodePointArray(afterString)
|
||||||
|
val distance = editDistance(beforeString, afterString)
|
||||||
|
val beforeLength = before.size
|
||||||
|
val afterLength = after.size
|
||||||
|
if (0 == beforeLength || 0 == afterLength) return 0.0f
|
||||||
|
var spaceCount = 0
|
||||||
|
for (j: Int in after) {
|
||||||
|
if (j == KeyEvent.KEYCODE_SPACE) ++spaceCount
|
||||||
|
}
|
||||||
|
if (spaceCount == afterLength) return 0.0f
|
||||||
|
if (score <= 0 || distance >= afterLength) {
|
||||||
|
// normalizedScore must be 0.0f (the minimum value) if the score is less than or equal to 0,
|
||||||
|
// or if the edit distance is larger than or equal to afterLength.
|
||||||
|
return 0.0f
|
||||||
|
}
|
||||||
|
// add a weight based on edit distance.
|
||||||
|
val weight = 1.0f - distance.toFloat() / afterLength.toFloat()
|
||||||
|
return score.toFloat() / 1000000.0f * weight
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun editDistance(x: String, y: String): Int {
|
||||||
|
val dp = Array(x.length + 1) {
|
||||||
|
IntArray(
|
||||||
|
y.length + 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
for (i in 0..x.length) {
|
||||||
|
for (j in 0..y.length) {
|
||||||
|
if (i == 0) {
|
||||||
|
dp[i][j] = j
|
||||||
|
} else if (j == 0) {
|
||||||
|
dp[i][j] = i
|
||||||
|
} else {
|
||||||
|
dp[i][j] = min(
|
||||||
|
dp[i - 1][j - 1]
|
||||||
|
+ costOfSubstitution(x[i - 1], y[j - 1]),
|
||||||
|
dp[i - 1][j] + 1,
|
||||||
|
dp[i][j - 1] + 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dp[x.length][y.length]
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun min(vararg numbers: Int): Int {
|
||||||
|
var min = Int.MAX_VALUE
|
||||||
|
for (n: Int in numbers) {
|
||||||
|
if (n < min) min = n
|
||||||
|
}
|
||||||
|
return min
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun costOfSubstitution(a: Char, b: Char): Int {
|
||||||
|
return if (a == b) 0 else 1
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,24 +1,50 @@
|
||||||
package org.dslul.openboard.inputmethod.latin
|
package org.dslul.openboard.inputmethod.latin
|
||||||
|
|
||||||
import android.content.Context
|
import androidx.core.content.edit
|
||||||
import android.util.LruCache
|
import androidx.test.runner.AndroidJUnit4
|
||||||
import org.dslul.openboard.inputmethod.keyboard.Keyboard
|
|
||||||
import org.dslul.openboard.inputmethod.latin.SuggestedWords.SuggestedWordInfo
|
import org.dslul.openboard.inputmethod.latin.SuggestedWords.SuggestedWordInfo
|
||||||
import org.dslul.openboard.inputmethod.latin.SuggestedWords.SuggestedWordInfo.KIND_FLAG_APPROPRIATE_FOR_AUTO_CORRECTION
|
import org.dslul.openboard.inputmethod.latin.SuggestedWords.SuggestedWordInfo.KIND_FLAG_APPROPRIATE_FOR_AUTO_CORRECTION
|
||||||
import org.dslul.openboard.inputmethod.latin.SuggestedWords.SuggestedWordInfo.KIND_WHITELIST
|
import org.dslul.openboard.inputmethod.latin.SuggestedWords.SuggestedWordInfo.KIND_WHITELIST
|
||||||
import org.dslul.openboard.inputmethod.latin.common.ComposedData
|
import org.dslul.openboard.inputmethod.latin.common.ComposedData
|
||||||
import org.dslul.openboard.inputmethod.latin.common.StringUtils
|
import org.dslul.openboard.inputmethod.latin.common.StringUtils
|
||||||
|
import org.dslul.openboard.inputmethod.latin.settings.Settings
|
||||||
import org.dslul.openboard.inputmethod.latin.settings.SettingsValuesForSuggestion
|
import org.dslul.openboard.inputmethod.latin.settings.SettingsValuesForSuggestion
|
||||||
|
import org.dslul.openboard.inputmethod.latin.utils.DeviceProtectedUtils
|
||||||
import org.dslul.openboard.inputmethod.latin.utils.SuggestionResults
|
import org.dslul.openboard.inputmethod.latin.utils.SuggestionResults
|
||||||
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.io.File
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.Robolectric
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
import org.robolectric.annotation.Implementation
|
||||||
|
import org.robolectric.annotation.Implements
|
||||||
|
import org.robolectric.shadows.ShadowLog
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
@Config(shadows = [
|
||||||
|
ShadowLocaleManagerCompat::class,
|
||||||
|
ShadowInputMethodManager2::class,
|
||||||
|
ShadowBinaryDictionaryUtils::class,
|
||||||
|
ShadowFacilitator::class,
|
||||||
|
])
|
||||||
class SuggestTest {
|
class SuggestTest {
|
||||||
private val thresholdModest = 0.185f
|
private lateinit var latinIME: LatinIME
|
||||||
private val thresholdAggressive = 0.067f
|
private val suggest get() = latinIME.mInputLogic.mSuggest
|
||||||
private val thresholdVeryAggressive = Float.NEGATIVE_INFINITY
|
|
||||||
|
// values taken from the string array auto_correction_threshold_mode_indexes
|
||||||
|
private val thresholdModest = "0"
|
||||||
|
private val thresholdAggressive = "1"
|
||||||
|
private val thresholdVeryAggressive = "2"
|
||||||
|
|
||||||
|
@Before fun setUp() {
|
||||||
|
latinIME = Robolectric.setupService(LatinIME::class.java)
|
||||||
|
// start logging only after latinIME is created, avoids showing the stack traces if library is not found
|
||||||
|
ShadowLog.setupLogging()
|
||||||
|
ShadowLog.stream = System.out
|
||||||
|
DeviceProtectedUtils.getSharedPreferences(latinIME)
|
||||||
|
.edit { putBoolean(Settings.PREF_AUTO_CORRECTION, true) } // need to enable, off by default
|
||||||
|
}
|
||||||
|
|
||||||
@Test fun `"on" to "in" if "in" was used before in this context`() {
|
@Test fun `"on" to "in" if "in" was used before in this context`() {
|
||||||
val locale = Locale.ENGLISH
|
val locale = Locale.ENGLISH
|
||||||
|
@ -197,9 +223,64 @@ class SuggestTest {
|
||||||
// not even allowed to check because of low score for ne
|
// not even allowed to check because of low score for ne
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setAutCorrectThreshold(threshold: String) {
|
||||||
|
val prefs = DeviceProtectedUtils.getSharedPreferences(latinIME)
|
||||||
|
prefs.edit { putString(Settings.PREF_AUTO_CORRECTION_CONFIDENCE, threshold) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun shouldBeAutoCorrected(word: String, // typed word
|
||||||
|
suggestions: List<SuggestedWordInfo>, // suggestions ordered by score, including suggestion for typed word if in dictionary
|
||||||
|
firstSuggestionForEmpty: SuggestedWordInfo?, // first suggestion if typed word would be empty (null if none)
|
||||||
|
typedWordSuggestionForEmpty: SuggestedWordInfo?, // suggestion for actually typed word if typed word would be empty (null if none)
|
||||||
|
typingLocale: Locale, // used for checking whether suggestion locale is the same, relevant e.g. for English i -> I shortcut, but we want Polish i
|
||||||
|
autoCorrectThreshold: String // 0, 1, or 2, but better use the vals on top with the corresponding name
|
||||||
|
): List<Boolean> {
|
||||||
|
setAutCorrectThreshold(autoCorrectThreshold)
|
||||||
|
currentTypingLocale = typingLocale
|
||||||
|
val suggestionsContainer = ArrayList<SuggestedWordInfo>().apply { addAll(suggestions) }
|
||||||
|
val suggestionResults = SuggestionResults(suggestions.size, false, false)
|
||||||
|
suggestions.forEach { suggestionResults.add(it) }
|
||||||
|
|
||||||
|
// store the original SuggestedWordInfo for typed word, as it will be removed
|
||||||
|
// we may want to re-add it in case auto-correction happens, so that the original word can at least be selected
|
||||||
|
var typedWordFirstOccurrenceWordInfo: SuggestedWordInfo? = null
|
||||||
|
var foundInDictionary = false
|
||||||
|
var sourceDictionaryOfRemovedWord: Dictionary? = null
|
||||||
|
for (info in suggestionsContainer) {
|
||||||
|
// Search for the best dictionary, defined as the first one with the highest match
|
||||||
|
// quality we can find.
|
||||||
|
if (!foundInDictionary && word == info.mWord) {
|
||||||
|
// Use this source if the old match had lower quality than this match
|
||||||
|
sourceDictionaryOfRemovedWord = info.mSourceDict
|
||||||
|
foundInDictionary = true
|
||||||
|
typedWordFirstOccurrenceWordInfo = info
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val firstOccurrenceOfTypedWordInSuggestions =
|
||||||
|
SuggestedWordInfo.removeDupsAndTypedWord(word, suggestionsContainer)
|
||||||
|
|
||||||
|
return suggest.shouldBeAutoCorrected(
|
||||||
|
StringUtils.getTrailingSingleQuotesCount(word),
|
||||||
|
word,
|
||||||
|
suggestionsContainer, // todo: get from suggestions? mostly it's just removing the typed word, right?
|
||||||
|
sourceDictionaryOfRemovedWord,
|
||||||
|
listOf(firstSuggestionForEmpty, typedWordSuggestionForEmpty),
|
||||||
|
{}, // only used to fill above if needed
|
||||||
|
true, // doesn't make sense otherwise
|
||||||
|
0, // not really relevant here
|
||||||
|
WordComposer.getComposerForTest(false),
|
||||||
|
suggestionResults,
|
||||||
|
firstOccurrenceOfTypedWordInSuggestions,
|
||||||
|
typedWordFirstOccurrenceWordInfo
|
||||||
|
).toList()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun suggestion(word: String, score: Int, locale: Locale) =
|
private var currentTypingLocale = Locale.ENGLISH
|
||||||
|
|
||||||
|
fun suggestion(word: String, score: Int, locale: Locale) =
|
||||||
SuggestedWordInfo(
|
SuggestedWordInfo(
|
||||||
/* word */ word,
|
/* word */ word,
|
||||||
/* prevWordsContext */ "", // irrelevant
|
/* prevWordsContext */ "", // irrelevant
|
||||||
|
@ -214,177 +295,14 @@ private fun suggestion(word: String, score: Int, locale: Locale) =
|
||||||
/* autoCommitFirstWordConfidence */ 0 // irrelevant?
|
/* autoCommitFirstWordConfidence */ 0 // irrelevant?
|
||||||
)
|
)
|
||||||
|
|
||||||
fun shouldBeAutoCorrected(word: String, // typed word
|
@Implements(DictionaryFacilitatorImpl::class)
|
||||||
suggestions: List<SuggestedWordInfo>, // suggestions ordered by score, including suggestion for typed word if in dictionary
|
class ShadowFacilitator {
|
||||||
firstSuggestionForEmpty: SuggestedWordInfo?, // first suggestion if typed word would be empty (null if none)
|
@Implementation
|
||||||
typedWordSuggestionForEmpty: SuggestedWordInfo?, // suggestion for actually typed word if typed word would be empty (null if none)
|
fun getCurrentLocale() = currentTypingLocale
|
||||||
currentTypingLocale: Locale, // used for checking whether suggestion locale is the same, relevant e.g. for English i -> I shortcut, but we want Polish i
|
@Implementation
|
||||||
autoCorrectThreshold: Float // -inf, 0.067, 0.185 (for very aggressive, aggressive, modest)
|
fun hasAtLeastOneInitializedMainDictionary() = true // otherwise no autocorrect
|
||||||
): List<Boolean> {
|
|
||||||
val suggestionsContainer = ArrayList<SuggestedWordInfo>().apply { addAll(suggestions) }
|
|
||||||
val suggestionResults = SuggestionResults(suggestions.size, false, false)
|
|
||||||
suggestions.forEach { suggestionResults.add(it) }
|
|
||||||
|
|
||||||
// store the original SuggestedWordInfo for typed word, as it will be removed
|
|
||||||
// we may want to re-add it in case auto-correction happens, so that the original word can at least be selected
|
|
||||||
var typedWordFirstOccurrenceWordInfo: SuggestedWordInfo? = null
|
|
||||||
var foundInDictionary = false
|
|
||||||
var sourceDictionaryOfRemovedWord: Dictionary? = null
|
|
||||||
for (info in suggestionsContainer) {
|
|
||||||
// Search for the best dictionary, defined as the first one with the highest match
|
|
||||||
// quality we can find.
|
|
||||||
if (!foundInDictionary && word == info.mWord) {
|
|
||||||
// Use this source if the old match had lower quality than this match
|
|
||||||
sourceDictionaryOfRemovedWord = info.mSourceDict
|
|
||||||
foundInDictionary = true
|
|
||||||
typedWordFirstOccurrenceWordInfo = info
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val firstOccurrenceOfTypedWordInSuggestions =
|
|
||||||
SuggestedWordInfo.removeDupsAndTypedWord(word, suggestionsContainer)
|
|
||||||
|
|
||||||
return Suggest.shouldBeAutoCorrected(
|
|
||||||
StringUtils.getTrailingSingleQuotesCount(word),
|
|
||||||
word,
|
|
||||||
suggestionsContainer, // todo: get from suggestions? mostly it's just removing the typed word, right?
|
|
||||||
sourceDictionaryOfRemovedWord,
|
|
||||||
listOf(firstSuggestionForEmpty, typedWordSuggestionForEmpty),
|
|
||||||
{}, // only used to fill above if needed
|
|
||||||
true, // doesn't make sense otherwise
|
|
||||||
0, // not really relevant here
|
|
||||||
WordComposer.getComposerForTest(false),
|
|
||||||
suggestionResults,
|
|
||||||
facilitator(currentTypingLocale),
|
|
||||||
autoCorrectThreshold,
|
|
||||||
firstOccurrenceOfTypedWordInSuggestions,
|
|
||||||
typedWordFirstOccurrenceWordInfo
|
|
||||||
).toList()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun facilitator(currentTypingLocale: Locale): DictionaryFacilitator =
|
|
||||||
object : DictionaryFacilitator {
|
|
||||||
override fun setValidSpellingWordReadCache(cache: LruCache<String, Boolean>?) {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
override fun setValidSpellingWordWriteCache(cache: LruCache<String, Boolean>?) {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
override fun isForLocale(locale: Locale?): Boolean {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
override fun isForAccount(account: String?): Boolean {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
override fun onStartInput() {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
override fun onFinishInput(context: Context?) {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
override fun isActive(): Boolean {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
override fun getLocale(): Locale {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
override fun getCurrentLocale(): Locale = currentTypingLocale
|
|
||||||
override fun usesContacts(): Boolean {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
override fun getAccount(): String {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
override fun resetDictionaries(
|
|
||||||
context: Context?,
|
|
||||||
newLocale: Locale?,
|
|
||||||
useContactsDict: Boolean,
|
|
||||||
usePersonalizedDicts: Boolean,
|
|
||||||
forceReloadMainDictionary: Boolean,
|
|
||||||
account: String?,
|
|
||||||
dictNamePrefix: String?,
|
|
||||||
listener: DictionaryFacilitator.DictionaryInitializationListener?
|
|
||||||
) {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
override fun removeWord(word: String?) {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
override fun resetDictionariesForTesting(
|
|
||||||
context: Context?,
|
|
||||||
locale: Locale?,
|
|
||||||
dictionaryTypes: java.util.ArrayList<String>?,
|
|
||||||
dictionaryFiles: HashMap<String, File>?,
|
|
||||||
additionalDictAttributes: MutableMap<String, MutableMap<String, String>>?,
|
|
||||||
account: String?
|
|
||||||
) {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
override fun closeDictionaries() {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
override fun getSubDictForTesting(dictName: String?): ExpandableBinaryDictionary {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
override fun hasAtLeastOneInitializedMainDictionary(): Boolean = true
|
|
||||||
override fun hasAtLeastOneUninitializedMainDictionary(): Boolean {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
override fun waitForLoadingMainDictionaries(timeout: Long, unit: TimeUnit?) {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
override fun waitForLoadingDictionariesForTesting(timeout: Long, unit: TimeUnit?) {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
override fun addToUserHistory(
|
|
||||||
suggestion: String?,
|
|
||||||
wasAutoCapitalized: Boolean,
|
|
||||||
ngramContext: NgramContext,
|
|
||||||
timeStampInSeconds: Long,
|
|
||||||
blockPotentiallyOffensive: Boolean
|
|
||||||
) {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
override fun unlearnFromUserHistory(
|
|
||||||
word: String?,
|
|
||||||
ngramContext: NgramContext,
|
|
||||||
timeStampInSeconds: Long,
|
|
||||||
eventType: Int
|
|
||||||
) {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
override fun getSuggestionResults(
|
|
||||||
composedData: ComposedData?,
|
|
||||||
ngramContext: NgramContext?,
|
|
||||||
keyboard: Keyboard,
|
|
||||||
settingsValuesForSuggestion: SettingsValuesForSuggestion?,
|
|
||||||
sessionId: Int,
|
|
||||||
inputStyle: Int
|
|
||||||
): SuggestionResults {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
override fun isValidSpellingWord(word: String?): Boolean {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
override fun isValidSuggestionWord(word: String?): Boolean {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
override fun clearUserHistoryDictionary(context: Context?): Boolean {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
override fun dump(context: Context?): String {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
override fun dumpDictionaryForDebug(dictName: String?) {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
override fun getDictionaryStats(context: Context?): MutableList<DictionaryStats> {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private class TestDict(locale: Locale) : Dictionary("testDict", locale) {
|
private class TestDict(locale: Locale) : Dictionary("testDict", locale) {
|
||||||
override fun getSuggestions(
|
override fun getSuggestions(
|
||||||
composedData: ComposedData?,
|
composedData: ComposedData?,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue