Skip to content

Commit eaa029d

Browse files
Add pure-Java gesture typing fallback (SwipeGestureEngine)
HeliBoard's gesture typing infrastructure (touch tracking, trail rendering, settings UI) has always been present, but word matching requires a native library — either the proprietary Google libjni_latinimegoogle.so or the open replacement being developed under the NLnet grant (#2226, #668). Users without a compatible library get no suggestions at all when swiping. This commit adds SwipeGestureEngine: a self-contained Java implementation that makes gesture typing work without any native library. Algorithm (arc-length resampling + L2 distance): - At index build time, each dictionary word is converted to a normalized gesture path: letter key centres are looked up from the live Keyboard object, deduplicated, then resampled to 16 evenly-spaced (x,y) points. Words are grouped by first letter for fast lookup. - At gesture end, the raw InputPointers stroke is resampled the same way. Candidates are filtered by first/last letter, then ranked by L2 distance to their precomputed path with a small log-frequency bonus so common words win ties. Integration points: - JniUtils: sHaveGestureLib is set to true unconditionally so the gesture toggle appears in settings and touch input is routed as batch input. - Suggest.getSuggestedWordsForBatchInput: builds the index lazily on the first gesture (background thread) and caches it; clears on dictionary or layout change. Never calls the JNI stub with SESSION_ID_GESTURE. - Dictionary hierarchy: getAllWordsWithFrequency() added to Dictionary, ReadOnlyBinaryDictionary (token-based JNI iteration with probability), and DictionaryCollection; DictionaryFacilitator/Impl expose it. Properties: - Pure Java, no new dependencies, ~270 lines. - Layout-aware: key positions are read from the live Keyboard object, so any user layout (QWERTY, Dvorak, custom) works correctly. - Index is rebuilt automatically on layout or dictionary change. - Coexists with the native library: if a native library is loaded, it takes over and this engine is dormant. This is intentionally a simple baseline — the NLnet native library (#2226) will eventually supersede it with higher accuracy. Until then this gives every HeliBoard user working gesture typing out of the box. Relates to: #668, #2226
1 parent 2fde28c commit eaa029d

8 files changed

Lines changed: 351 additions & 5 deletions

File tree

app/src/main/java/helium314/keyboard/latin/DictionaryFacilitator.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@
1919
import helium314.keyboard.latin.settings.SettingsValuesForSuggestion;
2020
import helium314.keyboard.latin.utils.SuggestionResults;
2121

22+
import java.util.Collections;
2223
import java.util.List;
2324
import java.util.Locale;
25+
import java.util.Map;
2426
import java.util.concurrent.TimeUnit;
2527

2628
/**
@@ -151,4 +153,14 @@ void unlearnFromUserHistory(final String word,
151153
void dumpDictionaryForDebug(final String dictName);
152154

153155
@NonNull List<DictionaryStats> getDictionaryStats(final Context context);
156+
157+
/**
158+
* Returns all words with frequencies from the primary main dictionary, for gesture typing
159+
* precomputation. Iterates the binary dictionary directly; can be slow on first call.
160+
* The default returns an empty map; DictionaryFacilitatorImpl overrides this.
161+
*/
162+
@NonNull
163+
default Map<String, Integer> getAllMainDictionaryWordsWithFrequency() {
164+
return Collections.emptyMap();
165+
}
154166
}

app/src/main/java/helium314/keyboard/latin/DictionaryFacilitatorImpl.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,9 @@ class DictionaryFacilitatorImpl : DictionaryFacilitator {
475475
putWordIntoValidSpellingWordCache("unlearnFromUserHistory", word.lowercase(Locale.getDefault()))
476476
}
477477

478+
override fun getAllMainDictionaryWordsWithFrequency(): Map<String, Int> =
479+
dictionaryGroups[0].getDict(Dictionary.TYPE_MAIN)?.getAllWordsWithFrequency() ?: emptyMap()
480+
478481
// TODO: Revise the way to fusion suggestion results.
479482
override fun getSuggestionResults(
480483
composedData: ComposedData, ngramContext: NgramContext, keyboard: Keyboard,

app/src/main/java/helium314/keyboard/latin/Suggest.kt

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import helium314.keyboard.latin.define.DebugFlags
1717
import helium314.keyboard.latin.define.DecoderSpecificConstants.SHOULD_AUTO_CORRECT_USING_NON_WHITE_LISTED_SUGGESTION
1818
import helium314.keyboard.latin.define.DecoderSpecificConstants.SHOULD_REMOVE_PREVIOUSLY_REJECTED_SUGGESTION
1919
import helium314.keyboard.latin.dictionary.Dictionary
20+
import helium314.keyboard.latin.gesture.SwipeGestureEngine
2021
import helium314.keyboard.latin.settings.Settings
2122
import helium314.keyboard.latin.settings.SettingsValuesForSuggestion
2223
import helium314.keyboard.latin.suggestions.SuggestionStripView
@@ -35,8 +36,14 @@ class Suggest(private val mDictionaryFacilitator: DictionaryFacilitator) {
3536
private val mPlausibilityThreshold = 0f
3637
private val nextWordSuggestionsCache = HashMap<NgramContext, SuggestionResults>()
3738

39+
// Precomputed gesture word index. Rebuilt lazily; cleared on dictionary/layout change.
40+
@Volatile private var gestureIndex: SwipeGestureEngine.GestureIndex? = null
41+
3842
// cache cleared whenever LatinIME.loadSettings is called, notably on changing layout and switching input fields
39-
fun clearNextWordSuggestionsCache() = nextWordSuggestionsCache.clear()
43+
fun clearNextWordSuggestionsCache() {
44+
nextWordSuggestionsCache.clear()
45+
gestureIndex = null
46+
}
4047

4148
/**
4249
* Set the normalized-score threshold for a suggestion to be considered strong enough that we
@@ -265,10 +272,17 @@ class Suggest(private val mDictionaryFacilitator: DictionaryFacilitator) {
265272
settingsValuesForSuggestion: SettingsValuesForSuggestion,
266273
inputStyle: Int, sequenceNumber: Int
267274
): SuggestedWords {
268-
val suggestionResults = mDictionaryFacilitator.getSuggestionResults(
269-
wordComposer.composedDataSnapshot, ngramContext, keyboard,
270-
settingsValuesForSuggestion, SESSION_ID_GESTURE, inputStyle
271-
)
275+
val pointers = wordComposer.composedDataSnapshot.mInputPointers
276+
// Build the precomputed gesture index lazily (once per dictionary/layout).
277+
// getAllMainDictionaryWordsWithFrequency() iterates the binary dict via JNI — slow on
278+
// first call, instant thereafter. Runs on InputLogicHandler's background thread.
279+
var index = gestureIndex
280+
if (index == null || index.byFirst.isEmpty()) {
281+
val words = mDictionaryFacilitator.getAllMainDictionaryWordsWithFrequency()
282+
index = SwipeGestureEngine.buildIndex(words, keyboard)
283+
if (index.byFirst.isNotEmpty()) gestureIndex = index
284+
}
285+
val suggestionResults = SwipeGestureEngine.rankByIndex(index, pointers, keyboard, SuggestedWords.MAX_SUGGESTIONS)
272286
replaceSingleLetterFirstSuggestion(suggestionResults)
273287

274288
// For transforming words that don't come from a dictionary, because it's our best bet

app/src/main/java/helium314/keyboard/latin/dictionary/Dictionary.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
package helium314.keyboard.latin.dictionary;
88

99
import java.util.ArrayList;
10+
import java.util.Collections;
1011
import java.util.Locale;
12+
import java.util.Map;
1113

1214
import helium314.keyboard.latin.NgramContext;
1315
import helium314.keyboard.latin.SuggestedWords.SuggestedWordInfo;
@@ -98,6 +100,16 @@ public boolean isValidWord(final String word) {
98100
*/
99101
abstract public boolean isInDictionary(final String word);
100102

103+
/**
104+
* Returns all words stored in this dictionary.
105+
* The default implementation returns an empty list; override in concrete dictionaries
106+
* that support full enumeration (e.g. ReadOnlyBinaryDictionary).
107+
*/
108+
@androidx.annotation.NonNull
109+
public Map<String, Integer> getAllWordsWithFrequency() {
110+
return Collections.emptyMap();
111+
}
112+
101113
/**
102114
* Get the frequency of the word.
103115
* @param word the word to get the frequency of.

app/src/main/java/helium314/keyboard/latin/dictionary/DictionaryCollection.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
import java.util.Arrays;
1717
import java.util.Collection;
1818
import java.util.Collections;
19+
import java.util.HashMap;
1920
import java.util.Locale;
21+
import java.util.Map;
2022

2123
/**
2224
* Class for a collection of dictionaries that behave like one dictionary.
@@ -89,6 +91,14 @@ public int getMaxFrequencyOfExactMatches(final String word) {
8991
return maxFreq;
9092
}
9193

94+
@Override
95+
@androidx.annotation.NonNull
96+
public Map<String, Integer> getAllWordsWithFrequency() {
97+
Map<String, Integer> result = new HashMap<>();
98+
for (Dictionary dict : mDictionaries) result.putAll(dict.getAllWordsWithFrequency());
99+
return result;
100+
}
101+
92102
@Override
93103
public boolean isInitialized() {
94104
return !mDictionaries.isEmpty();

app/src/main/java/helium314/keyboard/latin/dictionary/ReadOnlyBinaryDictionary.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
import helium314.keyboard.latin.settings.SettingsValuesForSuggestion;
1616

1717
import java.util.ArrayList;
18+
import java.util.HashMap;
1819
import java.util.Locale;
20+
import java.util.Map;
1921
import java.util.concurrent.locks.ReentrantReadWriteLock;
2022

2123
/**
@@ -109,6 +111,30 @@ public int getMaxFrequencyOfExactMatches(final String word) {
109111
return NOT_A_PROBABILITY;
110112
}
111113

114+
@Override
115+
@androidx.annotation.NonNull
116+
public Map<String, Integer> getAllWordsWithFrequency() {
117+
Map<String, Integer> words = new HashMap<>();
118+
if (!mLock.readLock().tryLock()) return words;
119+
try {
120+
int token = 0;
121+
do {
122+
BinaryDictionary.GetNextWordPropertyResult result =
123+
mBinaryDictionary.getNextWordProperty(token);
124+
if (result.mWordProperty == null) break;
125+
if (!result.mWordProperty.mIsNotAWord && !result.mWordProperty.mIsPossiblyOffensive) {
126+
String word = result.mWordProperty.mWord;
127+
if (word != null && !word.isEmpty())
128+
words.put(word, result.mWordProperty.mProbabilityInfo.mProbability);
129+
}
130+
token = result.mNextToken;
131+
} while (token != 0);
132+
} finally {
133+
mLock.readLock().unlock();
134+
}
135+
return words;
136+
}
137+
112138
@Override
113139
public WordProperty getWordProperty(String word, boolean isBeginningOfSentence) {
114140
if (mLock.readLock().tryLock()) {

0 commit comments

Comments
 (0)