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
|
||||
}
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests {
|
||||
includeAndroidResources = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
@ -64,5 +70,11 @@ dependencies {
|
|||
implementation 'androidx.recyclerview:recyclerview:1.3.1'
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation 'com.github.skydoves:colorpickerview:2.2.4'
|
||||
|
||||
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 {
|
||||
val ueh = Thread.getDefaultUncaughtExceptionHandler()
|
||||
check(ueh !is CrashReportExceptionHandler) { "May not install several CrashReportExceptionHandlers!" }
|
||||
if (ueh is CrashReportExceptionHandler)
|
||||
return false
|
||||
defaultUncaughtExceptionHandler = ueh
|
||||
Thread.setDefaultUncaughtExceptionHandler(this)
|
||||
return true
|
||||
|
|
|
@ -18,6 +18,7 @@ package org.dslul.openboard.inputmethod.latin;
|
|||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.dslul.openboard.inputmethod.annotations.UsedForTesting;
|
||||
import org.dslul.openboard.inputmethod.keyboard.Keyboard;
|
||||
import org.dslul.openboard.inputmethod.keyboard.KeyboardId;
|
||||
import org.dslul.openboard.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
|
||||
|
@ -203,8 +204,6 @@ public final class Suggest {
|
|||
keyboard.mId.mMode,
|
||||
wordComposer,
|
||||
suggestionResults,
|
||||
mDictionaryFacilitator,
|
||||
mAutoCorrectionThreshold,
|
||||
firstOccurrenceOfTypedWordInSuggestions,
|
||||
typedWordFirstOccurrenceWordInfo
|
||||
);
|
||||
|
@ -282,7 +281,8 @@ public final class Suggest {
|
|||
}
|
||||
|
||||
// returns [allowsToBeAutoCorrected, hasAutoCorrection]
|
||||
static boolean[] shouldBeAutoCorrected(
|
||||
@UsedForTesting
|
||||
boolean[] shouldBeAutoCorrected(
|
||||
final int trailingSingleQuotesCount,
|
||||
final String typedWordString,
|
||||
final List<SuggestedWordInfo> suggestionsContainer,
|
||||
|
@ -293,8 +293,6 @@ public final class Suggest {
|
|||
final int keyboardIdMode,
|
||||
final WordComposer wordComposer,
|
||||
final SuggestionResults suggestionResults,
|
||||
final DictionaryFacilitator dictionaryFacilitator,
|
||||
final float autoCorrectionThreshold,
|
||||
final int firstOccurrenceOfTypedWordInSuggestions,
|
||||
final SuggestedWordInfo typedWordFirstOccurrenceWordInfo
|
||||
) {
|
||||
|
@ -365,7 +363,7 @@ public final class Suggest {
|
|||
// 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.
|
||||
// TODO: now that we have personalization, we may want to re-evaluate this decision
|
||||
|| !dictionaryFacilitator.hasAtLeastOneInitializedMainDictionary()) {
|
||||
|| !mDictionaryFacilitator.hasAtLeastOneInitializedMainDictionary()) {
|
||||
hasAutoCorrection = false;
|
||||
} else {
|
||||
final SuggestedWordInfo firstSuggestion = suggestionResults.first();
|
||||
|
@ -376,7 +374,7 @@ public final class Suggest {
|
|||
return new boolean[]{ true, true };
|
||||
}
|
||||
if (!AutoCorrectionUtils.suggestionExceedsThreshold(
|
||||
firstSuggestion, consideredWord, autoCorrectionThreshold)) {
|
||||
firstSuggestion, consideredWord, mAutoCorrectionThreshold)) {
|
||||
// todo: maybe also do something here depending on ngram context?
|
||||
// Score is too low for autocorrect
|
||||
return new boolean[]{ true, false };
|
||||
|
@ -390,7 +388,7 @@ public final class Suggest {
|
|||
// typed word is valid and has good score
|
||||
// do not auto-correct if typed word is better match than first suggestion
|
||||
final SuggestedWordInfo first = firstSuggestionInContainer != null ? firstSuggestionInContainer : firstSuggestion;
|
||||
final Locale dictLocale = dictionaryFacilitator.getCurrentLocale();
|
||||
final Locale dictLocale = mDictionaryFacilitator.getCurrentLocale();
|
||||
|
||||
if (first.mScore < scoreLimit) {
|
||||
// don't allow if suggestion has too low score
|
||||
|
|
|
@ -16,18 +16,11 @@
|
|||
|
||||
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 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.common.StringUtils;
|
||||
import org.dslul.openboard.inputmethod.latin.define.DebugFlags;
|
||||
|
||||
public final class AutoCorrectionUtils {
|
||||
|
@ -38,7 +31,6 @@ public final class AutoCorrectionUtils {
|
|||
// Purely static class: can't instantiate.
|
||||
}
|
||||
|
||||
@SuppressLint("ObsoleteSdkInt") // SDK_INT is 0 in unit tests
|
||||
public static boolean suggestionExceedsThreshold(final SuggestedWordInfo suggestion,
|
||||
final String consideredWord, final float threshold) {
|
||||
if (null != suggestion) {
|
||||
|
@ -53,13 +45,7 @@ public final class AutoCorrectionUtils {
|
|||
final int autoCorrectionSuggestionScore = suggestion.mScore;
|
||||
// TODO: when the normalized score of the first suggestion is nearly equals to
|
||||
// the normalized score of the second suggestion, behave less aggressive.
|
||||
final float normalizedScore;
|
||||
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(
|
||||
final float normalizedScore = BinaryDictionaryUtils.calcNormalizedScore(
|
||||
consideredWord, suggestion.mWord, autoCorrectionSuggestionScore);
|
||||
if (DBG) {
|
||||
Log.d(TAG, "Normalized " + consideredWord + "," + suggestion + ","
|
||||
|
@ -75,72 +61,4 @@ public final class AutoCorrectionUtils {
|
|||
}
|
||||
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
|
||||
|
||||
import android.content.Context
|
||||
import android.util.LruCache
|
||||
import org.dslul.openboard.inputmethod.keyboard.Keyboard
|
||||
import androidx.core.content.edit
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
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_WHITELIST
|
||||
import org.dslul.openboard.inputmethod.latin.common.ComposedData
|
||||
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.utils.DeviceProtectedUtils
|
||||
import org.dslul.openboard.inputmethod.latin.utils.SuggestionResults
|
||||
import org.junit.Before
|
||||
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.concurrent.TimeUnit
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@Config(shadows = [
|
||||
ShadowLocaleManagerCompat::class,
|
||||
ShadowInputMethodManager2::class,
|
||||
ShadowBinaryDictionaryUtils::class,
|
||||
ShadowFacilitator::class,
|
||||
])
|
||||
class SuggestTest {
|
||||
private val thresholdModest = 0.185f
|
||||
private val thresholdAggressive = 0.067f
|
||||
private val thresholdVeryAggressive = Float.NEGATIVE_INFINITY
|
||||
private lateinit var latinIME: LatinIME
|
||||
private val suggest get() = latinIME.mInputLogic.mSuggest
|
||||
|
||||
// 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`() {
|
||||
val locale = Locale.ENGLISH
|
||||
|
@ -197,9 +223,64 @@ class SuggestTest {
|
|||
// 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(
|
||||
/* word */ word,
|
||||
/* prevWordsContext */ "", // irrelevant
|
||||
|
@ -214,177 +295,14 @@ private fun suggestion(word: String, score: Int, locale: Locale) =
|
|||
/* autoCommitFirstWordConfidence */ 0 // irrelevant?
|
||||
)
|
||||
|
||||
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)
|
||||
currentTypingLocale: Locale, // used for checking whether suggestion locale is the same, relevant e.g. for English i -> I shortcut, but we want Polish i
|
||||
autoCorrectThreshold: Float // -inf, 0.067, 0.185 (for very aggressive, aggressive, modest)
|
||||
): 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()
|
||||
@Implements(DictionaryFacilitatorImpl::class)
|
||||
class ShadowFacilitator {
|
||||
@Implementation
|
||||
fun getCurrentLocale() = currentTypingLocale
|
||||
@Implementation
|
||||
fun hasAtLeastOneInitializedMainDictionary() = true // otherwise no autocorrect
|
||||
}
|
||||
|
||||
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) {
|
||||
override fun getSuggestions(
|
||||
composedData: ComposedData?,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue