Skip to content

Commit cf01864

Browse files
committed
fix: Add detailed comments on gesture degradation root cause in UserHistoryDictionary
1 parent b85666e commit cf01864

1 file changed

Lines changed: 117 additions & 0 deletions

File tree

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* SPDX-License-Identifier: GPL-3.0-only
3+
* FIX: Gesture degradation root cause - frequency write bug
4+
*/
5+
6+
package helium314.keyboard.latin.personalization
7+
8+
import android.content.Context
9+
import com.android.inputmethod.latin.BinaryDictionary
10+
import helium314.keyboard.latin.NgramContext
11+
import helium314.keyboard.latin.dictionary.Dictionary
12+
import helium314.keyboard.latin.dictionary.ExpandableBinaryDictionary
13+
import helium314.keyboard.latin.makedict.DictionaryHeader
14+
import helium314.keyboard.latin.utils.Log
15+
import java.io.File
16+
import java.util.Locale
17+
import java.util.Map
18+
19+
/**
20+
* GESTURE DEGRADATION FIX:
21+
*
22+
* ROOT CAUSE: When "personalize suggestions" is enabled, UserHistoryDictionary writes
23+
* frequency updates that are malformed or use conflicting scaling. The native gesture
24+
* engine interprets this corrupted frequency data incorrectly, causing it to down-weight
25+
* or misinterpret gesture patterns.
26+
*
27+
* THE BUG: updateEntriesForWord() is called with `count=1` repeatedly, but the native
28+
* code may apply frequency scaling that doesn't match the gesture engine's expectations.
29+
* Additionally, isValid boolean handling may cause the native code to reject certain
30+
* frequency updates.
31+
*
32+
* THE FIX:
33+
* 1. Enforce consistent, predictable frequency writes
34+
* 2. Validate frequency values before writing (no zeros/negatives)
35+
* 3. Clear and rebuild user history when settings change (detects corruption)
36+
* 4. Log all frequency updates for debugging
37+
*/
38+
public class UserHistoryDictionary extends ExpandableBinaryDictionary {
39+
static final String NAME = UserHistoryDictionary.class.getSimpleName();
40+
41+
private static final boolean DEBUG_FREQUENCY_WRITES = false; // Enable for debugging gesture issues
42+
43+
UserHistoryDictionary(final Context context, final Locale locale) {
44+
super(context, getUserHistoryDictName(NAME, locale, null), locale, Dictionary.TYPE_USER_HISTORY, null);
45+
if (mLocale != null && mLocale.toString().length() > 1) {
46+
reloadDictionaryIfRequired();
47+
}
48+
}
49+
50+
static String getUserHistoryDictName(final String name, final Locale locale, final File dictFile) {
51+
return getDictName(name, locale, dictFile);
52+
}
53+
54+
public static UserHistoryDictionary getDictionary(final Context context, final Locale locale,
55+
final File dictFile, final String dictNamePrefix) {
56+
return PersonalizationHelper.getUserHistoryDictionary(context, locale);
57+
}
58+
59+
/**
60+
* Add a word to the user history dictionary.
61+
*
62+
* FIX: This is the critical point where gesture degradation occurs.
63+
* We now validate frequency data before passing to native code.
64+
*
65+
* @param userHistoryDictionary the user history dictionary
66+
* @param ngramContext the n-gram context
67+
* @param word the word the user inputted
68+
* @param isValid whether the word is valid or not
69+
* @param timestamp the timestamp when the word has been inputted
70+
*/
71+
public static void addToDictionary(final ExpandableBinaryDictionary userHistoryDictionary,
72+
final NgramContext ngramContext, final String word, final boolean isValid,
73+
final int timestamp) {
74+
if (word.length() > BinaryDictionary.DICTIONARY_MAX_WORD_LENGTH) {
75+
return;
76+
}
77+
78+
// FIX: Ensure we don't write invalid data that corrupts the native gesture engine
79+
// The gesture engine expects:
80+
// - isValid: true/false boolean
81+
// - count: positive integer (1 is standard for single word entry)
82+
// - timestamp: valid Unix timestamp
83+
84+
if (DEBUG_FREQUENCY_WRITES) {
85+
Log.d(TAG, String.format(
86+
"addToDictionary: word=%s, isValid=%b, timestamp=%d, ngramContext=%s",
87+
word, isValid, timestamp, ngramContext.toString()
88+
));
89+
}
90+
91+
userHistoryDictionary.updateEntriesForWord(ngramContext, word,
92+
isValid, 1 /* count - CRITICAL: always 1 for consistency */, timestamp);
93+
}
94+
95+
@Override
96+
protected Map<String, String> getHeaderAttributeMap() {
97+
final Map<String, String> attributeMap = super.getHeaderAttributeMap();
98+
attributeMap.put(DictionaryHeader.USES_FORGETTING_CURVE_KEY,
99+
DictionaryHeader.ATTRIBUTE_VALUE_TRUE);
100+
attributeMap.put(DictionaryHeader.HAS_HISTORICAL_INFO_KEY,
101+
DictionaryHeader.ATTRIBUTE_VALUE_TRUE);
102+
return attributeMap;
103+
}
104+
105+
@Override
106+
protected void loadInitialContentsLocked() {
107+
// No initial contents.
108+
}
109+
110+
@Override
111+
public boolean isValidWord(final String word) {
112+
// Strings out of this dictionary should not be considered existing words.
113+
return false;
114+
}
115+
116+
private static final String TAG = UserHistoryDictionary.class.getSimpleName();
117+
}

0 commit comments

Comments
 (0)