2019-12-31 18:19:35 +01:00
/ *
2023-10-17 13:44:01 +02:00
* Copyright ( C ) 2013 The Android Open Source Project
* modified
* SPDX - License - Identifier : Apache - 2 . 0 AND GPL - 3 . 0 - only
2019-12-31 18:19:35 +01:00
* /
2024-01-31 18:32:43 +01:00
package helium314.keyboard.latin ;
2019-12-31 18:19:35 +01:00
import android.Manifest ;
import android.content.Context ;
2023-06-28 21:18:41 +02:00
import android.provider.UserDictionary ;
2019-12-31 18:19:35 +01:00
import android.text.TextUtils ;
2024-01-23 12:36:35 +01:00
2024-05-02 16:17:46 +02:00
import helium314.keyboard.latin.common.StringUtilsKt ;
2024-01-31 18:32:43 +01:00
import helium314.keyboard.latin.settings.SettingsValues ;
2024-02-01 08:51:17 +01:00
import helium314.keyboard.latin.utils.DeviceProtectedUtils ;
2024-01-31 18:32:43 +01:00
import helium314.keyboard.latin.utils.Log ;
2019-12-31 18:19:35 +01:00
import android.util.LruCache ;
2023-09-01 08:08:36 +02:00
import androidx.annotation.NonNull ;
import androidx.annotation.Nullable ;
2024-01-31 18:32:43 +01:00
import helium314.keyboard.keyboard.Keyboard ;
import helium314.keyboard.latin.NgramContext.WordInfo ;
import helium314.keyboard.latin.SuggestedWords.SuggestedWordInfo ;
import helium314.keyboard.latin.common.ComposedData ;
import helium314.keyboard.latin.common.Constants ;
import helium314.keyboard.latin.common.StringUtils ;
import helium314.keyboard.latin.permissions.PermissionsUtil ;
import helium314.keyboard.latin.personalization.UserHistoryDictionary ;
import helium314.keyboard.latin.settings.Settings ;
import helium314.keyboard.latin.settings.SettingsValuesForSuggestion ;
import helium314.keyboard.latin.utils.ExecutorUtils ;
import helium314.keyboard.latin.utils.SuggestionResults ;
2019-12-31 18:19:35 +01:00
import java.io.File ;
2023-06-28 20:40:35 +02:00
import java.io.FileOutputStream ;
import java.io.IOException ;
import java.nio.charset.StandardCharsets ;
2019-12-31 18:19:35 +01:00
import java.util.ArrayList ;
import java.util.Collections ;
import java.util.HashMap ;
import java.util.HashSet ;
import java.util.List ;
import java.util.Locale ;
import java.util.Map ;
2023-06-28 20:40:35 +02:00
import java.util.Scanner ;
import java.util.Set ;
2019-12-31 18:19:35 +01:00
import java.util.concurrent.ConcurrentHashMap ;
import java.util.concurrent.CountDownLatch ;
import java.util.concurrent.TimeUnit ;
/ * *
* Facilitates interaction with different kinds of dictionaries . Provides APIs
* to instantiate and select the correct dictionaries ( based on language or account ) ,
* update entries and fetch suggestions .
2024-01-01 17:42:26 +01:00
* < p >
2019-12-31 18:19:35 +01:00
* Currently AndroidSpellCheckerService and LatinIME both use DictionaryFacilitator as
* a client for interacting with dictionaries .
* /
public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
public static final String TAG = DictionaryFacilitatorImpl . class . getSimpleName ( ) ;
// HACK: This threshold is being used when adding a capitalized entry in the User History
// dictionary.
private static final int CAPITALIZED_FORM_MAX_PROBABILITY_FOR_INSERT = 140 ;
2024-01-29 16:00:24 +01:00
private ArrayList < DictionaryGroup > mDictionaryGroups = new ArrayList < > ( ) { { add ( new DictionaryGroup ( ) ) ; } } ;
2019-12-31 18:19:35 +01:00
private volatile CountDownLatch mLatchForWaitingLoadingMainDictionaries = new CountDownLatch ( 0 ) ;
// To synchronize assigning mDictionaryGroup to ensure closing dictionaries.
private final Object mLock = new Object ( ) ;
2023-09-11 11:39:02 +02:00
// library does not deal well with ngram history for auto-capitalized words, so we adjust the ngram
// context to store next word suggestions for such cases
private boolean mTryChangingWords = false ;
private String mChangeFrom = " " ;
private String mChangeTo = " " ;
2019-12-31 18:19:35 +01:00
2023-11-20 01:11:06 +01:00
// todo: write cache never set, and never read (only written)
2024-02-14 09:22:30 +01:00
// tried to use read cache for a while, but small performance improvements are not worth the work (https://github.com/Helium314/HeliBoard/issues/307)
2019-12-31 18:19:35 +01:00
private LruCache < String , Boolean > mValidSpellingWordReadCache ;
private LruCache < String , Boolean > mValidSpellingWordWriteCache ;
@Override
public void setValidSpellingWordReadCache ( final LruCache < String , Boolean > cache ) {
mValidSpellingWordReadCache = cache ;
}
@Override
public void setValidSpellingWordWriteCache ( final LruCache < String , Boolean > cache ) {
mValidSpellingWordWriteCache = cache ;
}
2023-08-11 09:03:16 +02:00
// judging by usage, this should check primary locale only
2019-12-31 18:19:35 +01:00
@Override
public boolean isForLocale ( final Locale locale ) {
2023-08-11 00:58:33 +02:00
return locale ! = null & & locale . equals ( mDictionaryGroups . get ( 0 ) . mLocale ) ;
2019-12-31 18:19:35 +01:00
}
2023-06-26 23:47:21 +02:00
private boolean hasLocale ( final Locale locale ) {
2023-08-11 00:58:33 +02:00
if ( locale = = null ) return false ;
for ( DictionaryGroup dictionaryGroup : mDictionaryGroups ) {
if ( locale . equals ( dictionaryGroup . mLocale ) ) return true ;
}
return false ;
2023-06-26 23:47:21 +02:00
}
2019-12-31 18:19:35 +01:00
/ * *
* Returns whether this facilitator is exactly for this account .
*
* @param account the account to test against .
* /
public boolean isForAccount ( @Nullable final String account ) {
2023-08-11 00:58:33 +02:00
return TextUtils . equals ( mDictionaryGroups . get ( 0 ) . mAccount , account ) ;
2019-12-31 18:19:35 +01:00
}
/ * *
* A group of dictionaries that work together for a single language .
* /
private static class DictionaryGroup {
2023-06-26 23:47:21 +02:00
private static final int MAX_CONFIDENCE = 2 ;
2019-12-31 18:19:35 +01:00
/ * *
* The locale associated with the dictionary group .
* /
2023-09-15 10:55:47 +02:00
@NonNull public final Locale mLocale ;
2019-12-31 18:19:35 +01:00
/ * *
* The user account associated with the dictionary group .
* /
@Nullable public final String mAccount ;
@Nullable private Dictionary mMainDict ;
// Confidence that the most probable language is actually the language the user is
// typing in. For now, this is simply the number of times a word from this language
2023-06-26 23:47:21 +02:00
// has been committed in a row, with an exception when typing a single word not contained
// in this language.
private int mConfidence = 1 ;
2023-06-28 20:40:35 +02:00
// words cannot be removed from main dictionary, so we use a blacklist instead
public String blacklistFileName = null ;
public Set < String > blacklist = new HashSet < > ( ) ;
2023-06-26 23:47:21 +02:00
// allow to go above max confidence, for better determination of currently preferred language
// when decreasing confidence or getting weight factor, limit to maximum
public void increaseConfidence ( ) {
mConfidence + = 1 ;
}
// If confidence is above max, drop to max confidence. This does not change weights and
// allows conveniently typing single words from the other language without affecting suggestions
public void decreaseConfidence ( ) {
if ( mConfidence > MAX_CONFIDENCE )
mConfidence = MAX_CONFIDENCE ;
2023-11-22 22:41:41 +01:00
else if ( mConfidence > 0 ) {
2023-06-26 23:47:21 +02:00
mConfidence - = 1 ;
}
}
2023-11-22 22:41:41 +01:00
public float getWeightForTypingInLocale ( List < DictionaryGroup > groups ) {
return getWeightForLocale ( groups , 0 . 15f ) ;
2023-06-26 23:47:21 +02:00
}
2019-12-31 18:19:35 +01:00
2023-11-22 22:41:41 +01:00
public float getWeightForGesturingInLocale ( List < DictionaryGroup > groups ) {
return getWeightForLocale ( groups , 0 . 05f ) ;
}
// might need some more tuning
private float getWeightForLocale ( final List < DictionaryGroup > groups , final float step ) {
if ( groups . size ( ) = = 1 ) return 1f ;
if ( mConfidence < 2 ) return 1f - step * ( MAX_CONFIDENCE - mConfidence ) ;
for ( DictionaryGroup group : groups ) {
if ( group ! = this & & group . mConfidence > = mConfidence ) return 1f - step / 2f ;
}
return 1f ;
}
2019-12-31 18:19:35 +01:00
public final ConcurrentHashMap < String , ExpandableBinaryDictionary > mSubDictMap =
new ConcurrentHashMap < > ( ) ;
public DictionaryGroup ( ) {
2023-11-27 14:50:36 +01:00
this ( new Locale ( " " ) , null , null , Collections . emptyMap ( ) ) ;
2019-12-31 18:19:35 +01:00
}
2023-09-15 10:55:47 +02:00
public DictionaryGroup ( @NonNull final Locale locale ,
2019-12-31 18:19:35 +01:00
@Nullable final Dictionary mainDict ,
@Nullable final String account ,
2023-09-10 21:39:56 +02:00
@NonNull final Map < String , ExpandableBinaryDictionary > subDicts ) {
2019-12-31 18:19:35 +01:00
mLocale = locale ;
mAccount = account ;
// The main dictionary can be asynchronously loaded.
setMainDict ( mainDict ) ;
for ( final Map . Entry < String , ExpandableBinaryDictionary > entry : subDicts . entrySet ( ) ) {
setSubDict ( entry . getKey ( ) , entry . getValue ( ) ) ;
}
}
2023-09-10 21:39:56 +02:00
private void setSubDict ( @NonNull final String dictType , @NonNull final ExpandableBinaryDictionary dict ) {
mSubDictMap . put ( dictType , dict ) ;
2019-12-31 18:19:35 +01:00
}
2023-09-10 21:39:56 +02:00
public void setMainDict ( @Nullable final Dictionary mainDict ) {
2019-12-31 18:19:35 +01:00
// Close old dictionary if exists. Main dictionary can be assigned multiple times.
final Dictionary oldDict = mMainDict ;
mMainDict = mainDict ;
if ( oldDict ! = null & & mainDict ! = oldDict ) {
oldDict . close ( ) ;
}
}
2023-09-10 21:39:56 +02:00
public @Nullable Dictionary getDict ( @NonNull final String dictType ) {
2019-12-31 18:19:35 +01:00
if ( Dictionary . TYPE_MAIN . equals ( dictType ) ) {
return mMainDict ;
}
return getSubDict ( dictType ) ;
}
2023-09-10 21:39:56 +02:00
public @Nullable ExpandableBinaryDictionary getSubDict ( @NonNull final String dictType ) {
2019-12-31 18:19:35 +01:00
return mSubDictMap . get ( dictType ) ;
}
2023-09-10 21:39:56 +02:00
public boolean hasDict ( @NonNull final String dictType , @Nullable final String account ) {
2019-12-31 18:19:35 +01:00
if ( Dictionary . TYPE_MAIN . equals ( dictType ) ) {
return mMainDict ! = null ;
}
if ( Dictionary . TYPE_USER_HISTORY . equals ( dictType ) & &
! TextUtils . equals ( account , mAccount ) ) {
// If the dictionary type is user history, & if the account doesn't match,
// return immediately. If the account matches, continue looking it up in the
// sub dictionary map.
return false ;
}
return mSubDictMap . containsKey ( dictType ) ;
}
2023-09-10 21:39:56 +02:00
public void closeDict ( @NonNull final String dictType ) {
2019-12-31 18:19:35 +01:00
final Dictionary dict ;
if ( Dictionary . TYPE_MAIN . equals ( dictType ) ) {
dict = mMainDict ;
} else {
dict = mSubDictMap . remove ( dictType ) ;
}
if ( dict ! = null ) {
dict . close ( ) ;
}
}
}
public DictionaryFacilitatorImpl ( ) {
}
@Override
public void onStartInput ( ) {
}
@Override
public void onFinishInput ( Context context ) {
2023-08-11 00:58:33 +02:00
for ( DictionaryGroup dictionaryGroup : mDictionaryGroups ) {
2023-06-26 23:47:21 +02:00
for ( final String dictType : ALL_DICTIONARY_TYPES ) {
2023-08-11 00:58:33 +02:00
Dictionary dict = dictionaryGroup . getDict ( dictType ) ;
2023-06-26 23:47:21 +02:00
if ( dict ! = null ) dict . onFinishInput ( ) ;
}
2023-08-11 00:58:33 +02:00
}
2019-12-31 18:19:35 +01:00
}
@Override
public boolean isActive ( ) {
2023-09-15 10:55:47 +02:00
return ! mDictionaryGroups . get ( 0 ) . mLocale . getLanguage ( ) . isEmpty ( ) ;
2019-12-31 18:19:35 +01:00
}
@Override
2024-02-06 20:55:15 +01:00
@NonNull
2024-02-03 10:59:46 +01:00
public Locale getMainLocale ( ) {
2023-08-11 00:58:33 +02:00
return mDictionaryGroups . get ( 0 ) . mLocale ;
2019-12-31 18:19:35 +01:00
}
2023-08-17 16:00:41 +02:00
@Override
public Locale getCurrentLocale ( ) {
return getCurrentlyPreferredDictionaryGroup ( ) . mLocale ;
}
2019-12-31 18:19:35 +01:00
@Override
public boolean usesContacts ( ) {
2023-08-11 00:58:33 +02:00
return mDictionaryGroups . get ( 0 ) . getSubDict ( Dictionary . TYPE_CONTACTS ) ! = null ;
2019-12-31 18:19:35 +01:00
}
2023-11-20 09:44:52 +01:00
@Override
public boolean usesPersonalization ( ) {
return mDictionaryGroups . get ( 0 ) . getSubDict ( Dictionary . TYPE_USER_HISTORY ) ! = null ;
}
2019-12-31 18:19:35 +01:00
@Override
public String getAccount ( ) {
return null ;
}
@Nullable
private static ExpandableBinaryDictionary getSubDict ( final String dictType ,
final Context context , final Locale locale , final File dictFile ,
final String dictNamePrefix , @Nullable final String account ) {
2024-01-28 11:00:56 +01:00
ExpandableBinaryDictionary dict = null ;
2019-12-31 18:19:35 +01:00
try {
2024-01-28 11:00:56 +01:00
dict = switch ( dictType ) {
case Dictionary . TYPE_USER_HISTORY - > UserHistoryDictionary . getDictionary ( context , locale , dictFile , dictNamePrefix , account ) ;
case Dictionary . TYPE_USER - > UserBinaryDictionary . getDictionary ( context , locale , dictFile , dictNamePrefix , account ) ;
case Dictionary . TYPE_CONTACTS - > ContactsBinaryDictionary . getDictionary ( context , locale , dictFile , dictNamePrefix , account ) ;
default - > null ;
} ;
} catch ( final SecurityException | IllegalArgumentException e ) {
2019-12-31 18:19:35 +01:00
Log . e ( TAG , " Cannot create dictionary: " + dictType , e ) ;
}
2024-01-28 11:00:56 +01:00
if ( dict = = null )
Log . e ( TAG , " Cannot create dictionary for " + dictType ) ;
return dict ;
2019-12-31 18:19:35 +01:00
}
@Nullable
2023-08-11 00:58:33 +02:00
static DictionaryGroup findDictionaryGroupWithLocale ( final List < DictionaryGroup > dictionaryGroups ,
2023-09-15 10:55:47 +02:00
@NonNull final Locale locale ) {
2023-08-11 00:58:33 +02:00
if ( dictionaryGroups = = null ) return null ;
for ( DictionaryGroup dictionaryGroup : dictionaryGroups ) {
2023-09-15 10:55:47 +02:00
if ( locale . equals ( dictionaryGroup . mLocale ) )
2023-08-11 00:58:33 +02:00
return dictionaryGroup ;
}
return null ;
2019-12-31 18:19:35 +01:00
}
2023-08-11 00:58:33 +02:00
// original
2019-12-31 18:19:35 +01:00
public void resetDictionaries (
final Context context ,
2023-09-15 10:55:47 +02:00
@NonNull final Locale newLocale ,
2019-12-31 18:19:35 +01:00
final boolean useContactsDict ,
final boolean usePersonalizedDicts ,
final boolean forceReloadMainDictionary ,
@Nullable final String account ,
final String dictNamePrefix ,
@Nullable final DictionaryInitializationListener listener ) {
final HashMap < Locale , ArrayList < String > > existingDictionariesToCleanup = new HashMap < > ( ) ;
final HashSet < String > subDictTypesToUse = new HashSet < > ( ) ;
subDictTypesToUse . add ( Dictionary . TYPE_USER ) ;
2023-11-27 14:50:36 +01:00
final List < Locale > allLocales = new ArrayList < > ( ) { {
2023-08-11 00:58:33 +02:00
add ( newLocale ) ;
2024-02-01 08:51:17 +01:00
addAll ( Settings . getSecondaryLocales ( DeviceProtectedUtils . getSharedPreferences ( context ) , newLocale ) ) ;
2023-08-11 00:58:33 +02:00
} } ;
2019-12-31 18:19:35 +01:00
// Do not use contacts dictionary if we do not have permissions to read contacts.
2023-11-27 14:50:36 +01:00
if ( useContactsDict
& & PermissionsUtil . checkAllPermissionsGranted ( context , Manifest . permission . READ_CONTACTS ) ) {
2019-12-31 18:19:35 +01:00
subDictTypesToUse . add ( Dictionary . TYPE_CONTACTS ) ;
}
if ( usePersonalizedDicts ) {
subDictTypesToUse . add ( Dictionary . TYPE_USER_HISTORY ) ;
}
2023-08-11 00:58:33 +02:00
// Gather all dictionaries by locale. We may remove some from the list to clean up later.
for ( DictionaryGroup dictionaryGroup : mDictionaryGroups ) {
final ArrayList < String > dictTypeForLocale = new ArrayList < > ( ) ;
existingDictionariesToCleanup . put ( dictionaryGroup . mLocale , dictTypeForLocale ) ;
2019-12-31 18:19:35 +01:00
for ( final String dictType : DYNAMIC_DICTIONARY_TYPES ) {
2023-08-11 00:58:33 +02:00
if ( dictionaryGroup . hasDict ( dictType , account ) ) {
2019-12-31 18:19:35 +01:00
dictTypeForLocale . add ( dictType ) ;
}
}
2023-08-11 00:58:33 +02:00
if ( dictionaryGroup . hasDict ( Dictionary . TYPE_MAIN , account ) ) {
2019-12-31 18:19:35 +01:00
dictTypeForLocale . add ( Dictionary . TYPE_MAIN ) ;
}
}
2023-08-11 00:58:33 +02:00
// create new dictionary groups and remove dictionaries to re-use from existingDictionariesToCleanup
final ArrayList < DictionaryGroup > newDictionaryGroups = new ArrayList < > ( allLocales . size ( ) ) ;
for ( Locale locale : allLocales ) {
// get existing dictionary group for new locale
2023-09-15 10:55:47 +02:00
final DictionaryGroup oldDictionaryGroupForLocale = findDictionaryGroupWithLocale ( mDictionaryGroups , locale ) ;
final ArrayList < String > dictTypesToCleanupForLocale = existingDictionariesToCleanup . get ( locale ) ;
2023-08-11 00:58:33 +02:00
final boolean noExistingDictsForThisLocale = ( null = = oldDictionaryGroupForLocale ) ;
// create new or re-use already loaded main dict
final Dictionary mainDict ;
if ( forceReloadMainDictionary | | noExistingDictsForThisLocale
| | ! oldDictionaryGroupForLocale . hasDict ( Dictionary . TYPE_MAIN , account ) ) {
mainDict = null ;
2019-12-31 18:19:35 +01:00
} else {
2023-08-11 00:58:33 +02:00
mainDict = oldDictionaryGroupForLocale . getDict ( Dictionary . TYPE_MAIN ) ;
dictTypesToCleanupForLocale . remove ( Dictionary . TYPE_MAIN ) ;
2019-12-31 18:19:35 +01:00
}
2023-08-11 00:58:33 +02:00
// create new or re-use already loaded sub-dicts
final Map < String , ExpandableBinaryDictionary > subDicts = new HashMap < > ( ) ;
2023-06-26 23:47:21 +02:00
for ( final String subDictType : subDictTypesToUse ) {
2023-08-11 00:58:33 +02:00
final ExpandableBinaryDictionary subDict ;
2023-10-31 12:06:44 +01:00
if ( noExistingDictsForThisLocale | | forceReloadMainDictionary
2023-08-11 00:58:33 +02:00
| | ! oldDictionaryGroupForLocale . hasDict ( subDictType , account ) ) {
// Create a new dictionary.
2023-11-27 14:50:36 +01:00
subDict = getSubDict ( subDictType , context , locale , null , dictNamePrefix , account ) ;
2024-02-14 09:22:30 +01:00
if ( subDict = = null ) continue ; // https://github.com/Helium314/HeliBoard/issues/293
2023-08-11 00:58:33 +02:00
} else {
// Reuse the existing dictionary, and don't close it at the end
subDict = oldDictionaryGroupForLocale . getSubDict ( subDictType ) ;
dictTypesToCleanupForLocale . remove ( subDictType ) ;
}
subDicts . put ( subDictType , subDict ) ;
2023-06-26 23:47:21 +02:00
}
2023-08-11 00:58:33 +02:00
DictionaryGroup newDictGroup = new DictionaryGroup ( locale , mainDict , account , subDicts ) ;
newDictionaryGroups . add ( newDictGroup ) ;
// load blacklist
if ( noExistingDictsForThisLocale ) {
2024-01-28 10:42:42 +01:00
newDictGroup . blacklistFileName = context . getFilesDir ( ) . getAbsolutePath ( ) + File . separator + " blacklists " + File . separator + locale . toLanguageTag ( ) + " .txt " ;
2023-08-11 00:58:33 +02:00
if ( ! new File ( newDictGroup . blacklistFileName ) . exists ( ) )
new File ( context . getFilesDir ( ) . getAbsolutePath ( ) + File . separator + " blacklists " ) . mkdirs ( ) ;
newDictGroup . blacklist . addAll ( readBlacklistFile ( newDictGroup . blacklistFileName ) ) ;
2023-06-26 23:47:21 +02:00
} else {
2023-08-11 00:58:33 +02:00
// re-use if possible
newDictGroup . blacklistFileName = oldDictionaryGroupForLocale . blacklistFileName ;
newDictGroup . blacklist . addAll ( oldDictionaryGroupForLocale . blacklist ) ;
2023-06-26 23:47:21 +02:00
}
}
2023-08-11 00:58:33 +02:00
2019-12-31 18:19:35 +01:00
// Replace Dictionaries.
2023-08-11 00:58:33 +02:00
final List < DictionaryGroup > oldDictionaryGroups ;
2019-12-31 18:19:35 +01:00
synchronized ( mLock ) {
2023-08-11 00:58:33 +02:00
oldDictionaryGroups = mDictionaryGroups ;
mDictionaryGroups = newDictionaryGroups ;
2019-12-31 18:19:35 +01:00
if ( hasAtLeastOneUninitializedMainDictionary ( ) ) {
2023-08-11 00:58:33 +02:00
asyncReloadUninitializedMainDictionaries ( context , allLocales , listener ) ;
2019-12-31 18:19:35 +01:00
}
}
2023-06-28 20:40:35 +02:00
2019-12-31 18:19:35 +01:00
if ( listener ! = null ) {
listener . onUpdateMainDictionaryAvailability ( hasAtLeastOneInitializedMainDictionary ( ) ) ;
}
// Clean up old dictionaries.
for ( final Locale localeToCleanUp : existingDictionariesToCleanup . keySet ( ) ) {
2024-01-01 17:42:26 +01:00
final ArrayList < String > dictTypesToCleanUp = existingDictionariesToCleanup . get ( localeToCleanUp ) ;
final DictionaryGroup dictionarySetToCleanup = findDictionaryGroupWithLocale ( oldDictionaryGroups , localeToCleanUp ) ;
2019-12-31 18:19:35 +01:00
for ( final String dictType : dictTypesToCleanUp ) {
dictionarySetToCleanup . closeDict ( dictType ) ;
}
}
if ( mValidSpellingWordWriteCache ! = null ) {
mValidSpellingWordWriteCache . evictAll ( ) ;
}
2023-11-20 01:11:06 +01:00
if ( mValidSpellingWordReadCache ! = null ) {
mValidSpellingWordReadCache . evictAll ( ) ;
}
2019-12-31 18:19:35 +01:00
}
private void asyncReloadUninitializedMainDictionaries ( final Context context ,
2023-08-11 00:58:33 +02:00
final List < Locale > locales , final DictionaryInitializationListener listener ) {
2019-12-31 18:19:35 +01:00
final CountDownLatch latchForWaitingLoadingMainDictionary = new CountDownLatch ( 1 ) ;
mLatchForWaitingLoadingMainDictionaries = latchForWaitingLoadingMainDictionary ;
2023-09-10 21:39:56 +02:00
ExecutorUtils . getBackgroundExecutor ( ExecutorUtils . KEYBOARD ) . execute ( ( ) - >
doReloadUninitializedMainDictionaries ( context , locales , listener , latchForWaitingLoadingMainDictionary ) ) ;
2019-12-31 18:19:35 +01:00
}
2023-08-11 00:58:33 +02:00
void doReloadUninitializedMainDictionaries ( final Context context , final List < Locale > locales ,
final DictionaryInitializationListener listener ,
2019-12-31 18:19:35 +01:00
final CountDownLatch latchForWaitingLoadingMainDictionary ) {
2023-08-11 00:58:33 +02:00
final Dictionary [ ] mainDicts = new Dictionary [ locales . size ( ) ] ;
final ArrayList < DictionaryGroup > dictionaryGroups = new ArrayList < > ( ) ;
for ( int i = 0 ; i < locales . size ( ) ; i + + ) {
Locale locale = locales . get ( i ) ;
DictionaryGroup dictionaryGroup = findDictionaryGroupWithLocale ( mDictionaryGroups , locale ) ;
if ( null = = dictionaryGroup ) {
// This should never happen, but better safe than crashy
Log . w ( TAG , " Expected a dictionary group for " + locale + " but none found " ) ;
return ;
}
dictionaryGroups . add ( dictionaryGroup ) ;
// do nothing if main dict already initialized
if ( dictionaryGroup . mMainDict ! = null & & dictionaryGroup . mMainDict . isInitialized ( ) ) {
mainDicts [ i ] = null ;
continue ;
}
2024-01-28 10:42:42 +01:00
mainDicts [ i ] = DictionaryFactoryKt . createMainDictionary ( context , dictionaryGroup . mLocale ) ;
2019-12-31 18:19:35 +01:00
}
2023-06-26 23:47:21 +02:00
2019-12-31 18:19:35 +01:00
synchronized ( mLock ) {
2023-08-11 00:58:33 +02:00
for ( int i = 0 ; i < locales . size ( ) ; i + + ) {
final Locale locale = locales . get ( i ) ;
if ( mainDicts [ i ] = = null )
continue ;
if ( locale . equals ( dictionaryGroups . get ( i ) . mLocale ) ) {
dictionaryGroups . get ( i ) . setMainDict ( mainDicts [ i ] ) ;
} else {
// Dictionary facilitator has been reset for another locale.
mainDicts [ i ] . close ( ) ;
}
2019-12-31 18:19:35 +01:00
}
}
if ( listener ! = null ) {
listener . onUpdateMainDictionaryAvailability ( hasAtLeastOneInitializedMainDictionary ( ) ) ;
}
latchForWaitingLoadingMainDictionary . countDown ( ) ;
}
public void closeDictionaries ( ) {
2023-08-11 00:58:33 +02:00
final ArrayList < DictionaryGroup > dictionaryGroupsToClose ;
2019-12-31 18:19:35 +01:00
synchronized ( mLock ) {
2023-08-11 00:58:33 +02:00
dictionaryGroupsToClose = new ArrayList < > ( mDictionaryGroups ) ;
mDictionaryGroups . clear ( ) ;
mDictionaryGroups . add ( new DictionaryGroup ( ) ) ;
2019-12-31 18:19:35 +01:00
}
2023-08-11 00:58:33 +02:00
for ( DictionaryGroup dictionaryGroup : dictionaryGroupsToClose ) {
for ( final String dictType : ALL_DICTIONARY_TYPES ) {
dictionaryGroup . closeDict ( dictType ) ;
}
2019-12-31 18:19:35 +01:00
}
}
// The main dictionaries are loaded asynchronously. Don't cache the return value
// of these methods.
public boolean hasAtLeastOneInitializedMainDictionary ( ) {
2023-08-11 00:58:33 +02:00
for ( DictionaryGroup dictionaryGroup : mDictionaryGroups ) {
final Dictionary mainDict = dictionaryGroup . getDict ( Dictionary . TYPE_MAIN ) ;
if ( mainDict ! = null & & mainDict . isInitialized ( ) ) return true ;
}
return false ;
2019-12-31 18:19:35 +01:00
}
public boolean hasAtLeastOneUninitializedMainDictionary ( ) {
2023-08-11 00:58:33 +02:00
for ( DictionaryGroup dictionaryGroup : mDictionaryGroups ) {
final Dictionary mainDict = dictionaryGroup . getDict ( Dictionary . TYPE_MAIN ) ;
if ( mainDict = = null | | ! mainDict . isInitialized ( ) ) return true ;
2023-06-26 23:47:21 +02:00
}
2023-08-11 00:58:33 +02:00
return false ;
2019-12-31 18:19:35 +01:00
}
public void waitForLoadingMainDictionaries ( final long timeout , final TimeUnit unit )
throws InterruptedException {
mLatchForWaitingLoadingMainDictionaries . await ( timeout , unit ) ;
}
public void addToUserHistory ( final String suggestion , final boolean wasAutoCapitalized ,
2023-09-01 08:08:36 +02:00
@NonNull final NgramContext ngramContext , final long timeStampInSeconds ,
2019-12-31 18:19:35 +01:00
final boolean blockPotentiallyOffensive ) {
// Update the spelling cache before learning. Words that are not yet added to user history
// and appear in no other language model are not considered valid.
putWordIntoValidSpellingWordCache ( " addToUserHistory " , suggestion ) ;
final String [ ] words = suggestion . split ( Constants . WORD_SEPARATOR ) ;
2023-06-26 23:47:21 +02:00
2023-08-11 00:58:33 +02:00
// increase / decrease confidence if we have more than one dictionary group
boolean [ ] validWordForDictionary ; // store results to avoid unnecessary duplicate lookups
2023-12-11 10:33:19 +01:00
if ( mDictionaryGroups . size ( ) > 1 & & words . length = = 1 ) { // ignore if more than a single word, this only happens with (badly working) spaceAwareGesture
2023-12-20 09:00:43 +01:00
validWordForDictionary = adjustConfidencesInternal ( suggestion , wasAutoCapitalized ) ;
2023-08-11 00:58:33 +02:00
} else
validWordForDictionary = null ;
// add word to user dictionary if it is in no other dictionary except user history dictionary,
2023-06-28 21:18:41 +02:00
// reasoning: typing the same word again -> we probably want it in some dictionary permanently
2024-01-23 12:36:35 +01:00
final SettingsValues sv = Settings . getInstance ( ) . getCurrent ( ) ;
if ( sv . mAddToPersonalDictionary // require the setting
2024-02-14 09:22:30 +01:00
& & sv . mAutoCorrectEnabled = = sv . mAutoCorrectionEnabledPerUserSettings // don't add if user wants autocorrect but input field does not, see https://github.com/Helium314/HeliBoard/issues/427#issuecomment-1905438000
2023-12-11 10:33:19 +01:00
& & mDictionaryGroups . get ( 0 ) . hasDict ( Dictionary . TYPE_USER_HISTORY , mDictionaryGroups . get ( 0 ) . mAccount ) // require personalized suggestions
& & ! wasAutoCapitalized // we can't be 100% sure about what the user intended to type, so better don't add it
& & words . length = = 1 ) { // ignore if more than a single word, this only happens with (badly working) spaceAwareGesture
2023-08-11 00:58:33 +02:00
addToPersonalDictionaryIfInvalidButInHistory ( suggestion , validWordForDictionary ) ;
2023-06-28 21:18:41 +02:00
}
2019-12-31 18:19:35 +01:00
NgramContext ngramContextForCurrentWord = ngramContext ;
for ( int i = 0 ; i < words . length ; i + + ) {
final String currentWord = words [ i ] ;
2020-01-26 23:19:47 +01:00
final boolean wasCurrentWordAutoCapitalized = ( i = = 0 ) & & wasAutoCapitalized ;
2023-06-26 23:47:21 +02:00
// add to history for preferred dictionary group, to avoid mixing languages in history
addWordToUserHistory ( getCurrentlyPreferredDictionaryGroup ( ) , ngramContextForCurrentWord , currentWord ,
2019-12-31 18:19:35 +01:00
wasCurrentWordAutoCapitalized , ( int ) timeStampInSeconds ,
blockPotentiallyOffensive ) ;
ngramContextForCurrentWord =
ngramContextForCurrentWord . getNextNgramContext ( new WordInfo ( currentWord ) ) ;
2023-06-28 20:40:35 +02:00
2023-08-11 00:58:33 +02:00
// remove manually entered blacklisted words from blacklist
for ( DictionaryGroup dictionaryGroup : mDictionaryGroups ) {
if ( dictionaryGroup . blacklist . remove ( currentWord ) )
removeWordFromBlacklistFile ( currentWord , dictionaryGroup . blacklistFileName ) ;
}
2019-12-31 18:19:35 +01:00
}
}
2023-12-20 09:00:43 +01:00
@Override public void adjustConfidences ( final String word , final boolean wasAutoCapitalized ) {
if ( mDictionaryGroups . size ( ) > 1 & & ! word . contains ( Constants . WORD_SEPARATOR ) )
adjustConfidencesInternal ( word , wasAutoCapitalized ) ;
}
private boolean [ ] adjustConfidencesInternal ( final String word , final boolean wasAutoCapitalized ) {
final boolean [ ] validWordForDictionary = new boolean [ mDictionaryGroups . size ( ) ] ;
// if suggestion was auto-capitalized, check against both the suggestion and the de-capitalized suggestion
final String decapitalizedSuggestion ;
if ( wasAutoCapitalized )
2024-05-02 16:17:46 +02:00
decapitalizedSuggestion = StringUtilsKt . decapitalize ( word , getCurrentLocale ( ) ) ;
2023-12-20 09:00:43 +01:00
else
decapitalizedSuggestion = word ;
for ( int i = 0 ; i < mDictionaryGroups . size ( ) ; i + + ) {
final DictionaryGroup dictionaryGroup = mDictionaryGroups . get ( i ) ;
final boolean isValidWord = isValidWord ( word , ALL_DICTIONARY_TYPES , dictionaryGroup ) ;
if ( isValidWord | | ( wasAutoCapitalized & & isValidWord ( decapitalizedSuggestion , ALL_DICTIONARY_TYPES , dictionaryGroup ) ) )
dictionaryGroup . increaseConfidence ( ) ;
else dictionaryGroup . decreaseConfidence ( ) ;
validWordForDictionary [ i ] = isValidWord ;
}
return validWordForDictionary ;
}
2023-07-08 09:11:22 +02:00
// main and secondary isValid provided to avoid duplicate lookups
2023-08-11 00:58:33 +02:00
private void addToPersonalDictionaryIfInvalidButInHistory ( String suggestion , boolean [ ] validWordForDictionary ) {
2023-10-24 16:07:03 +02:00
final DictionaryGroup dictionaryGroup = getClearlyPreferredDictionaryGroupOrNull ( ) ;
if ( dictionaryGroup = = null ) return ;
2023-08-11 00:58:33 +02:00
if ( validWordForDictionary = = null
? isValidWord ( suggestion , ALL_DICTIONARY_TYPES , dictionaryGroup )
2023-10-24 16:07:03 +02:00
: validWordForDictionary [ mDictionaryGroups . indexOf ( dictionaryGroup ) ]
2023-08-11 00:58:33 +02:00
)
return ;
2023-07-08 09:11:22 +02:00
final ExpandableBinaryDictionary userDict = dictionaryGroup . getSubDict ( Dictionary . TYPE_USER ) ;
final Dictionary userHistoryDict = dictionaryGroup . getSubDict ( Dictionary . TYPE_USER_HISTORY ) ;
2023-08-11 00:58:33 +02:00
// user history always reports words as invalid, so here we need to check isInDictionary instead
// also maybe a problem: words added to dictionaries (user and history) are apparently found
// only after some delay. but this is not too bad, it just delays adding
2023-07-08 09:11:22 +02:00
if ( userDict ! = null & & userHistoryDict . isInDictionary ( suggestion ) ) {
2023-08-11 00:58:33 +02:00
if ( userDict . isInDictionary ( suggestion ) ) // is this check necessary?
2023-07-08 09:11:22 +02:00
return ;
2023-09-10 21:39:56 +02:00
ExecutorUtils . getBackgroundExecutor ( ExecutorUtils . KEYBOARD ) . execute ( ( ) - >
2023-07-08 09:11:22 +02:00
UserDictionary . Words . addWord ( userDict . mContext , suggestion ,
2023-09-10 21:39:56 +02:00
250 /*FREQUENCY_FOR_USER_DICTIONARY_ADDS*/ , null , dictionaryGroup . mLocale ) ) ;
2023-07-08 09:11:22 +02:00
}
}
2019-12-31 18:19:35 +01:00
private void putWordIntoValidSpellingWordCache (
2023-09-01 08:08:36 +02:00
@NonNull final String caller ,
@NonNull final String originalWord ) {
2019-12-31 18:19:35 +01:00
if ( mValidSpellingWordWriteCache = = null ) {
return ;
}
2024-02-03 10:59:46 +01:00
final String lowerCaseWord = originalWord . toLowerCase ( getCurrentLocale ( ) ) ;
2019-12-31 18:19:35 +01:00
final boolean lowerCaseValid = isValidSpellingWord ( lowerCaseWord ) ;
mValidSpellingWordWriteCache . put ( lowerCaseWord , lowerCaseValid ) ;
final String capitalWord =
2024-02-03 10:59:46 +01:00
StringUtils . capitalizeFirstAndDowncaseRest ( originalWord , getCurrentLocale ( ) ) ;
2019-12-31 18:19:35 +01:00
final boolean capitalValid ;
if ( lowerCaseValid ) {
// The lower case form of the word is valid, so the upper case must be valid.
capitalValid = true ;
} else {
capitalValid = isValidSpellingWord ( capitalWord ) ;
}
mValidSpellingWordWriteCache . put ( capitalWord , capitalValid ) ;
}
private void addWordToUserHistory ( final DictionaryGroup dictionaryGroup ,
final NgramContext ngramContext , final String word , final boolean wasAutoCapitalized ,
final int timeStampInSeconds , final boolean blockPotentiallyOffensive ) {
final ExpandableBinaryDictionary userHistoryDictionary =
dictionaryGroup . getSubDict ( Dictionary . TYPE_USER_HISTORY ) ;
2023-06-26 23:47:21 +02:00
if ( userHistoryDictionary = = null | | ! hasLocale ( userHistoryDictionary . mLocale ) ) {
2019-12-31 18:19:35 +01:00
return ;
}
2023-06-26 23:47:21 +02:00
final int maxFreq = getFrequency ( word , dictionaryGroup ) ;
2019-12-31 18:19:35 +01:00
if ( maxFreq = = 0 & & blockPotentiallyOffensive ) {
return ;
}
2023-09-11 11:39:02 +02:00
if ( mTryChangingWords )
mTryChangingWords = ngramContext . changeWordIfAfterBeginningOfSentence ( mChangeFrom , mChangeTo ) ;
2019-12-31 18:19:35 +01:00
final String secondWord ;
if ( wasAutoCapitalized ) {
2023-06-26 23:47:21 +02:00
// used word with lower-case first letter instead of all lower-case, as auto-capitalize
// does not affect the other letters
2024-05-02 16:17:46 +02:00
final String decapitalizedWord = StringUtilsKt . decapitalize ( word , dictionaryGroup . mLocale ) ;
2023-06-26 23:47:21 +02:00
if ( isValidWord ( word , ALL_DICTIONARY_TYPES , dictionaryGroup ) & & ! isValidWord ( decapitalizedWord , ALL_DICTIONARY_TYPES , dictionaryGroup ) ) {
2019-12-31 18:19:35 +01:00
// If the word was auto-capitalized and exists only as a capitalized word in the
// dictionary, then we must not downcase it before registering it. For example,
// the name of the contacts in start-of-sentence position would come here with the
// wasAutoCapitalized flag: if we downcase it, we'd register a lower-case version
// of that contact's name which would end up popping in suggestions.
secondWord = word ;
} else {
2023-06-26 23:47:21 +02:00
// If however the word is not in the dictionary, or exists as a de-capitalized word
2019-12-31 18:19:35 +01:00
// only, then we consider that was a lower-case word that had been auto-capitalized.
2023-06-26 23:47:21 +02:00
secondWord = decapitalizedWord ;
2023-09-11 11:39:02 +02:00
mTryChangingWords = true ;
mChangeFrom = word ;
mChangeTo = secondWord ;
2019-12-31 18:19:35 +01:00
}
} else {
// HACK: We'd like to avoid adding the capitalized form of common words to the User
// History dictionary in order to avoid suggesting them until the dictionary
// consolidation is done.
// TODO: Remove this hack when ready.
2023-06-26 23:47:21 +02:00
final String lowerCasedWord = word . toLowerCase ( dictionaryGroup . mLocale ) ;
2019-12-31 18:19:35 +01:00
final int lowerCaseFreqInMainDict = dictionaryGroup . hasDict ( Dictionary . TYPE_MAIN ,
null /* account */ ) ?
dictionaryGroup . getDict ( Dictionary . TYPE_MAIN ) . getFrequency ( lowerCasedWord ) :
Dictionary . NOT_A_PROBABILITY ;
if ( maxFreq < lowerCaseFreqInMainDict
& & lowerCaseFreqInMainDict > = CAPITALIZED_FORM_MAX_PROBABILITY_FOR_INSERT ) {
// Use lower cased word as the word can be a distracter of the popular word.
secondWord = lowerCasedWord ;
} else {
secondWord = word ;
}
}
// We demote unrecognized words (frequency < 0, below) by specifying them as "invalid".
// We don't add words with 0-frequency (assuming they would be profanity etc.).
final boolean isValid = maxFreq > 0 ;
UserHistoryDictionary . addToDictionary ( userHistoryDictionary , ngramContext , secondWord ,
isValid , timeStampInSeconds ) ;
}
2023-08-11 00:58:33 +02:00
/** returns the dictionaryGroup with most confidence, first group when tied */
2023-06-26 23:47:21 +02:00
private DictionaryGroup getCurrentlyPreferredDictionaryGroup ( ) {
2023-08-11 00:58:33 +02:00
DictionaryGroup dictGroup = null ;
int highestConfidence = - 1 ;
for ( DictionaryGroup dictionaryGroup : mDictionaryGroups ) {
if ( dictionaryGroup . mConfidence > highestConfidence ) {
dictGroup = dictionaryGroup ;
highestConfidence = dictGroup . mConfidence ;
}
}
2023-06-26 23:47:21 +02:00
return dictGroup ;
}
2023-10-24 16:07:03 +02:00
private DictionaryGroup getClearlyPreferredDictionaryGroupOrNull ( ) {
// we want one clearly preferred group and return null otherwise
if ( mDictionaryGroups . size ( ) = = 1 )
return mDictionaryGroups . get ( 0 ) ;
// that preferred group should have at least MAX_CONFIDENCE
int highestGroup = - 1 ;
int highestGroupConfidence = DictionaryGroup . MAX_CONFIDENCE - 1 ;
for ( int i = 0 ; i < mDictionaryGroups . size ( ) ; i + + ) {
final DictionaryGroup dictionaryGroup = mDictionaryGroups . get ( i ) ;
if ( dictionaryGroup . mConfidence > highestGroupConfidence ) {
highestGroup = i ;
highestGroupConfidence = dictionaryGroup . mConfidence ;
} else if ( dictionaryGroup . mConfidence = = highestGroupConfidence ) {
highestGroup = - 1 ; // unset group on a tie
}
}
if ( highestGroup = = - 1 ) return null ;
return mDictionaryGroups . get ( highestGroup ) ;
}
2019-12-31 18:19:35 +01:00
private void removeWord ( final String dictName , final String word ) {
2023-06-26 23:47:21 +02:00
final ExpandableBinaryDictionary dictionary = getCurrentlyPreferredDictionaryGroup ( ) . getSubDict ( dictName ) ;
2019-12-31 18:19:35 +01:00
if ( dictionary ! = null ) {
dictionary . removeUnigramEntryDynamically ( word ) ;
}
}
@Override
public void unlearnFromUserHistory ( final String word ,
2023-09-01 08:08:36 +02:00
@NonNull final NgramContext ngramContext , final long timeStampInSeconds ,
2019-12-31 18:19:35 +01:00
final int eventType ) {
// TODO: Decide whether or not to remove the word on EVENT_BACKSPACE.
if ( eventType ! = Constants . EVENT_BACKSPACE ) {
removeWord ( Dictionary . TYPE_USER_HISTORY , word ) ;
}
// Update the spelling cache after unlearning. Words that are removed from user history
// and appear in no other language model are not considered valid.
putWordIntoValidSpellingWordCache ( " unlearnFromUserHistory " , word . toLowerCase ( ) ) ;
}
// TODO: Revise the way to fusion suggestion results.
@Override
2023-09-10 21:39:56 +02:00
@SuppressWarnings ( " unchecked " )
2023-09-01 08:08:36 +02:00
@NonNull public SuggestionResults getSuggestionResults ( ComposedData composedData ,
NgramContext ngramContext , @NonNull final Keyboard keyboard ,
2019-12-31 18:19:35 +01:00
SettingsValuesForSuggestion settingsValuesForSuggestion , int sessionId ,
int inputStyle ) {
long proximityInfoHandle = keyboard . getProximityInfo ( ) . getNativeProximityInfo ( ) ;
final SuggestionResults suggestionResults = new SuggestionResults (
SuggestedWords . MAX_SUGGESTIONS , ngramContext . isBeginningOfSentenceContext ( ) ,
false /* firstSuggestionExceedsConfidenceThreshold */ ) ;
final float [ ] weightOfLangModelVsSpatialModel =
new float [ ] { Dictionary . NOT_A_WEIGHT_OF_LANG_MODEL_VS_SPATIAL_MODEL } ;
2023-06-26 23:47:21 +02:00
2023-08-11 00:58:33 +02:00
// start getting suggestions for non-main locales first, but in background
2023-08-25 22:27:13 +02:00
final ArrayList < SuggestedWordInfo > [ ] otherDictionarySuggestions = ( ArrayList < SuggestedWordInfo > [ ] ) new ArrayList [ mDictionaryGroups . size ( ) - 1 ] ;
2023-08-11 00:58:33 +02:00
final CountDownLatch waitForOtherDictionaries ;
if ( mDictionaryGroups . size ( ) > 1 ) {
waitForOtherDictionaries = new CountDownLatch ( mDictionaryGroups . size ( ) - 1 ) ;
for ( int i = 1 ; i < mDictionaryGroups . size ( ) ; i + + ) {
final DictionaryGroup dictionaryGroup = mDictionaryGroups . get ( i ) ;
final int index = i - 1 ;
2023-08-25 22:27:13 +02:00
ExecutorUtils . getBackgroundExecutor ( ExecutorUtils . KEYBOARD ) . execute ( ( ) - > {
otherDictionarySuggestions [ index ] = getSuggestions ( composedData ,
ngramContext , settingsValuesForSuggestion , sessionId , proximityInfoHandle ,
weightOfLangModelVsSpatialModel , dictionaryGroup ) ;
waitForOtherDictionaries . countDown ( ) ;
2023-08-11 00:58:33 +02:00
} ) ;
}
} else
waitForOtherDictionaries = null ;
2023-06-26 23:47:21 +02:00
// get main locale suggestions
final ArrayList < SuggestedWordInfo > dictionarySuggestions = getSuggestions ( composedData ,
ngramContext , settingsValuesForSuggestion , sessionId , proximityInfoHandle ,
2023-08-11 00:58:33 +02:00
weightOfLangModelVsSpatialModel , mDictionaryGroups . get ( 0 ) ) ;
2023-06-26 23:47:21 +02:00
suggestionResults . addAll ( dictionarySuggestions ) ;
if ( null ! = suggestionResults . mRawSuggestions ) {
suggestionResults . mRawSuggestions . addAll ( dictionarySuggestions ) ;
}
2023-08-11 00:58:33 +02:00
// wait for other locale suggestions
if ( waitForOtherDictionaries ! = null ) {
try { waitForOtherDictionaries . await ( ) ; }
2023-06-26 23:47:21 +02:00
catch ( InterruptedException e ) {
Log . w ( TAG , " Interrupted while trying to get secondary locale suggestions " , e ) ;
}
2023-08-11 00:58:33 +02:00
for ( int i = 1 ; i < mDictionaryGroups . size ( ) ; i + + ) {
suggestionResults . addAll ( otherDictionarySuggestions [ i - 1 ] ) ;
if ( null ! = suggestionResults . mRawSuggestions ) {
suggestionResults . mRawSuggestions . addAll ( otherDictionarySuggestions [ i - 1 ] ) ;
}
2023-06-26 23:47:21 +02:00
}
}
return suggestionResults ;
}
private ArrayList < SuggestedWordInfo > getSuggestions ( ComposedData composedData ,
NgramContext ngramContext , SettingsValuesForSuggestion settingsValuesForSuggestion ,
int sessionId , long proximityInfoHandle , float [ ] weightOfLangModelVsSpatialModel ,
DictionaryGroup dictGroup ) {
final ArrayList < SuggestedWordInfo > suggestions = new ArrayList < > ( ) ;
2023-11-22 22:41:41 +01:00
float weightForLocale = composedData . mIsBatchMode
? dictGroup . getWeightForGesturingInLocale ( mDictionaryGroups )
: dictGroup . getWeightForTypingInLocale ( mDictionaryGroups ) ;
2019-12-31 18:19:35 +01:00
for ( final String dictType : ALL_DICTIONARY_TYPES ) {
2023-06-26 23:47:21 +02:00
final Dictionary dictionary = dictGroup . getDict ( dictType ) ;
2019-12-31 18:19:35 +01:00
if ( null = = dictionary ) continue ;
final ArrayList < SuggestedWordInfo > dictionarySuggestions =
dictionary . getSuggestions ( composedData , ngramContext ,
proximityInfoHandle , settingsValuesForSuggestion , sessionId ,
weightForLocale , weightOfLangModelVsSpatialModel ) ;
if ( null = = dictionarySuggestions ) continue ;
2023-06-28 20:40:35 +02:00
2023-11-27 16:51:39 +01:00
// for some reason, garbage words are produced when glide typing
// for user history and main dictionary we can filter them out by checking whether the
// dictionary actually contains the word
// but personal dictionary and addon dictionaries may contain shortcuts, which do not
// pass an isInDictionary check (e.g. emojis)
// (if the main dict contains shortcuts to non-words, this will break)
final boolean checkForGarbage = composedData . mIsBatchMode & & ( dictType . equals ( Dictionary . TYPE_USER_HISTORY ) | | dictType . equals ( Dictionary . TYPE_MAIN ) ) ;
2023-06-28 20:40:35 +02:00
for ( SuggestedWordInfo info : dictionarySuggestions ) {
2023-12-05 08:14:11 +01:00
final String word = info . getWord ( ) ;
if ( ! isBlacklisted ( word ) ) { // don't add blacklisted words
2023-11-27 16:51:39 +01:00
if ( checkForGarbage
2023-12-05 08:14:11 +01:00
// only check history and "main main dictionary"
// consider the user might use custom main dictionary containing shortcuts
// assume this is unlikely to happen, and take care about common shortcuts that are not actual words (emoji, symbols)
& & word . length ( ) > 2 // should exclude most symbol shortcuts
& & info . mSourceDict . mDictType . equals ( dictType ) // dictType is always main, but info.mSourceDict.mDictType contains the actual dict (main dict is a dictionary group)
& & ! StringUtils . mightBeEmoji ( word ) // emojis often have more than 2 chars; simplified check for performance reasons
& & ! dictionary . isInDictionary ( word ) )
2023-07-26 16:17:06 +02:00
continue ;
2023-06-28 20:40:35 +02:00
suggestions . add ( info ) ;
}
}
2019-12-31 18:19:35 +01:00
}
2023-06-26 23:47:21 +02:00
return suggestions ;
2019-12-31 18:19:35 +01:00
}
2023-06-26 23:47:21 +02:00
// Spell checker is using this, and has its own instance of DictionaryFacilitatorImpl,
// meaning that it always has default mConfidence. So we cannot choose to only check preferred
// locale, and instead simply return true if word is in any of the available dictionaries
2019-12-31 18:19:35 +01:00
public boolean isValidSpellingWord ( final String word ) {
if ( mValidSpellingWordReadCache ! = null ) {
final Boolean cachedValue = mValidSpellingWordReadCache . get ( word ) ;
if ( cachedValue ! = null ) {
return cachedValue ;
}
}
2023-11-20 01:11:06 +01:00
boolean result = false ;
2023-08-11 00:58:33 +02:00
for ( DictionaryGroup dictionaryGroup : mDictionaryGroups ) {
2023-11-20 01:11:06 +01:00
if ( isValidWord ( word , ALL_DICTIONARY_TYPES , dictionaryGroup ) ) {
result = true ;
break ;
}
2023-08-11 00:58:33 +02:00
}
2023-11-20 01:11:06 +01:00
if ( mValidSpellingWordReadCache ! = null )
mValidSpellingWordReadCache . put ( word , result ) ;
return result ;
2019-12-31 18:19:35 +01:00
}
2023-08-11 00:58:33 +02:00
// this is unused, so leave it for now (redirecting to isValidWord seems to defeat the purpose...)
2019-12-31 18:19:35 +01:00
public boolean isValidSuggestionWord ( final String word ) {
2023-08-11 00:58:33 +02:00
return isValidWord ( word , ALL_DICTIONARY_TYPES , mDictionaryGroups . get ( 0 ) ) ;
2019-12-31 18:19:35 +01:00
}
2023-06-26 23:47:21 +02:00
private boolean isValidWord ( final String word , final String [ ] dictionariesToCheck , final DictionaryGroup dictionaryGroup ) {
2019-12-31 18:19:35 +01:00
if ( TextUtils . isEmpty ( word ) ) {
return false ;
}
2023-06-28 20:40:35 +02:00
if ( isBlacklisted ( word ) ) return false ;
2019-12-31 18:19:35 +01:00
for ( final String dictType : dictionariesToCheck ) {
2023-06-26 23:47:21 +02:00
final Dictionary dictionary = dictionaryGroup . getDict ( dictType ) ;
2019-12-31 18:19:35 +01:00
// Ideally the passed map would come out of a {@link java.util.concurrent.Future} and
// would be immutable once it's finished initializing, but concretely a null test is
// probably good enough for the time being.
if ( null = = dictionary ) continue ;
if ( dictionary . isValidWord ( word ) ) {
return true ;
}
}
return false ;
}
2023-06-28 20:40:35 +02:00
private boolean isBlacklisted ( final String word ) {
2023-08-11 00:58:33 +02:00
for ( DictionaryGroup dictionaryGroup : mDictionaryGroups ) {
if ( dictionaryGroup . blacklist . contains ( word ) )
return true ;
}
2023-06-28 20:40:35 +02:00
return false ;
}
@Override
public void removeWord ( String word ) {
2023-08-11 00:58:33 +02:00
for ( DictionaryGroup dictionaryGroup : mDictionaryGroups ) {
removeWordFromGroup ( word , dictionaryGroup ) ;
}
2023-06-28 20:40:35 +02:00
}
private void removeWordFromGroup ( String word , DictionaryGroup group ) {
// remove from user history
final ExpandableBinaryDictionary historyDict = group . getSubDict ( Dictionary . TYPE_USER_HISTORY ) ;
if ( historyDict ! = null ) {
historyDict . removeUnigramEntryDynamically ( word ) ;
}
// and from personal dictionary
final ExpandableBinaryDictionary userDict = group . getSubDict ( Dictionary . TYPE_USER ) ;
if ( userDict ! = null ) {
userDict . removeUnigramEntryDynamically ( word ) ;
}
2023-09-01 20:23:54 +02:00
final ExpandableBinaryDictionary contactsDict = group . getSubDict ( Dictionary . TYPE_CONTACTS ) ;
if ( contactsDict ! = null ) {
2023-09-20 20:07:17 +02:00
if ( contactsDict . isInDictionary ( word ) ) {
2023-09-01 20:23:54 +02:00
contactsDict . removeUnigramEntryDynamically ( word ) ; // will be gone until next reload of dict
2023-09-20 20:07:17 +02:00
addToBlacklist ( word , group ) ;
return ;
}
}
if ( ! group . hasDict ( Dictionary . TYPE_MAIN , null ) )
return ;
if ( group . getDict ( Dictionary . TYPE_MAIN ) . isValidWord ( word ) ) {
addToBlacklist ( word , group ) ;
return ;
}
final String lowercase = word . toLowerCase ( group . mLocale ) ;
if ( group . getDict ( Dictionary . TYPE_MAIN ) . isValidWord ( lowercase ) ) {
addToBlacklist ( lowercase , group ) ;
2023-06-28 20:40:35 +02:00
}
}
2023-09-20 20:07:17 +02:00
private void addToBlacklist ( final String word , final DictionaryGroup group ) {
if ( ! group . blacklist . add ( word ) )
return ;
ExecutorUtils . getBackgroundExecutor ( ExecutorUtils . KEYBOARD ) . execute ( ( ) - > {
try {
FileOutputStream fos = new FileOutputStream ( group . blacklistFileName , true ) ;
fos . write ( ( word + " \ n " ) . getBytes ( StandardCharsets . UTF_8 ) ) ;
fos . close ( ) ;
} catch ( IOException e ) {
Log . e ( TAG , " Exception while trying to write blacklist " , e ) ;
}
} ) ;
}
2023-06-28 20:40:35 +02:00
private ArrayList < String > readBlacklistFile ( final String filename ) {
final ArrayList < String > blacklist = new ArrayList < > ( ) ;
if ( filename = = null ) return blacklist ;
File blacklistFile = new File ( filename ) ;
if ( ! blacklistFile . exists ( ) ) return blacklist ;
try {
final Scanner scanner = new Scanner ( blacklistFile , StandardCharsets . UTF_8 . name ( ) ) . useDelimiter ( " \ n " ) ;
while ( scanner . hasNext ( ) ) {
blacklist . add ( scanner . next ( ) ) ;
}
} catch ( IOException e ) {
Log . e ( TAG , " Exception while reading blacklist " , e ) ;
}
return blacklist ;
}
private void removeWordFromBlacklistFile ( String word , String filename ) {
2023-08-25 22:27:13 +02:00
ExecutorUtils . getBackgroundExecutor ( ExecutorUtils . KEYBOARD ) . execute ( ( ) - > {
try {
ArrayList < String > blacklist = readBlacklistFile ( filename ) ;
blacklist . remove ( word ) ;
FileOutputStream fos = new FileOutputStream ( filename ) ;
for ( String entry : blacklist ) {
fos . write ( ( entry + " \ n " ) . getBytes ( StandardCharsets . UTF_8 ) ) ;
2023-06-28 20:40:35 +02:00
}
2023-08-25 22:27:13 +02:00
fos . close ( ) ;
} catch ( IOException e ) {
Log . e ( TAG , " Exception while trying to write blacklist " + filename , e ) ;
2023-06-28 20:40:35 +02:00
}
} ) ;
}
2023-06-26 23:47:21 +02:00
// called from addWordToUserHistory with a specified dictionary, so provide this dictionary
private int getFrequency ( final String word , DictionaryGroup dictGroup ) {
2019-12-31 18:19:35 +01:00
if ( TextUtils . isEmpty ( word ) ) {
return Dictionary . NOT_A_PROBABILITY ;
}
int maxFreq = Dictionary . NOT_A_PROBABILITY ;
2023-06-26 23:47:21 +02:00
// ExpandableBinaryDictionary (means: all except main) always return NOT_A_PROBABILITY
// because it doesn't override getFrequency()
// So why is it checked anyway?
// Is this a bug, or intended by AOSP devs?
2019-12-31 18:19:35 +01:00
for ( final String dictType : ALL_DICTIONARY_TYPES ) {
2023-06-26 23:47:21 +02:00
final Dictionary dictionary = dictGroup . getDict ( dictType ) ;
2019-12-31 18:19:35 +01:00
if ( dictionary = = null ) continue ;
final int tempFreq = dictionary . getFrequency ( word ) ;
if ( tempFreq > = maxFreq ) {
maxFreq = tempFreq ;
}
}
return maxFreq ;
}
2024-01-29 16:00:24 +01:00
@Override
public boolean clearUserHistoryDictionary ( final Context context ) {
2023-08-11 00:58:33 +02:00
for ( DictionaryGroup dictionaryGroup : mDictionaryGroups ) {
2024-01-29 16:00:24 +01:00
final ExpandableBinaryDictionary dictionary = dictionaryGroup . getSubDict ( Dictionary . TYPE_USER_HISTORY ) ;
2023-08-11 00:58:33 +02:00
if ( dictionary = = null ) {
return false ; // should only ever happen for primary dictionary, so this is safe
}
dictionary . clear ( ) ;
2023-06-26 23:47:21 +02:00
}
2019-12-31 18:19:35 +01:00
return true ;
}
2023-11-22 21:51:48 +01:00
@Override
public String localesAndConfidences ( ) {
if ( mDictionaryGroups . size ( ) < 2 ) return null ;
final StringBuilder sb = new StringBuilder ( ) ;
for ( final DictionaryGroup dictGroup : mDictionaryGroups ) {
if ( sb . length ( ) > 0 )
sb . append ( " , " ) ;
sb . append ( dictGroup . mLocale ) . append ( " " ) . append ( dictGroup . mConfidence ) ;
}
return sb . toString ( ) ;
}
2019-12-31 18:19:35 +01:00
@Override
public void dumpDictionaryForDebug ( final String dictName ) {
2023-08-11 00:58:33 +02:00
final ExpandableBinaryDictionary dictToDump = mDictionaryGroups . get ( 0 ) . getSubDict ( dictName ) ;
2019-12-31 18:19:35 +01:00
if ( dictToDump = = null ) {
Log . e ( TAG , " Cannot dump " + dictName + " . "
+ " The dictionary is not being used for suggestion or cannot be dumped. " ) ;
return ;
}
dictToDump . dumpAllWordsForDebug ( ) ;
}
@Override
2023-08-11 00:58:33 +02:00
// this is unused, so leave it for now
2023-09-01 08:08:36 +02:00
@NonNull public List < DictionaryStats > getDictionaryStats ( final Context context ) {
2019-12-31 18:19:35 +01:00
final ArrayList < DictionaryStats > statsOfEnabledSubDicts = new ArrayList < > ( ) ;
for ( final String dictType : DYNAMIC_DICTIONARY_TYPES ) {
2023-08-11 00:58:33 +02:00
final ExpandableBinaryDictionary dictionary = mDictionaryGroups . get ( 0 ) . getSubDict ( dictType ) ;
2019-12-31 18:19:35 +01:00
if ( dictionary = = null ) continue ;
statsOfEnabledSubDicts . add ( dictionary . getDictionaryStats ( ) ) ;
}
return statsOfEnabledSubDicts ;
}
@Override
public String dump ( final Context context ) {
return " " ;
}
}