app names dictionary similar to contacts dictionary

This commit is contained in:
devycarol 2025-02-14 22:45:22 -07:00
parent a566e4d8a2
commit b0827c7967
17 changed files with 270 additions and 35 deletions

View file

@ -98,7 +98,13 @@ SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
<queries>
<!-- To detect other IMEs -->
<intent>
<action android:name="android.view.InputMethod"/>
<action android:name="android.view.InputMethod" />
</intent>
<!-- To detect names of installed apps -->
<intent>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent>
</queries>
</manifest>

View file

@ -0,0 +1,90 @@
// SPDX-License-Identifier: GPL-3.0-only
package helium314.keyboard.latin;
import android.content.Context;
import androidx.annotation.Nullable;
import com.android.inputmethod.latin.BinaryDictionary;
import java.io.File;
import java.util.Locale;
import helium314.keyboard.latin.common.StringUtils;
import helium314.keyboard.latin.utils.Log;
public class AppsBinaryDictionary extends ExpandableBinaryDictionary {
private static final String TAG = AppsBinaryDictionary.class.getSimpleName();
private static final String NAME = "apps";
private static final boolean DEBUG = false;
private static final boolean DEBUG_DUMP = false;
private final AppsManager mAppsManager;
protected AppsBinaryDictionary(final Context ctx, final Locale locale,
final File dictFile, final String name) {
super(ctx, getDictName(name, locale, dictFile), locale, Dictionary.TYPE_APPS, dictFile);
mAppsManager = new AppsManager(ctx);
reloadDictionaryIfRequired();
}
public static AppsBinaryDictionary getDictionary(final Context context, final Locale locale,
final File dictFile, final String dictNamePrefix, @Nullable final String account) {
return new AppsBinaryDictionary(context, locale, dictFile, dictNamePrefix + NAME);
}
/**
* Typically called whenever the dictionary is created for the first time or recreated when we
* think that there are updates to the dictionary. This is called asynchronously.
*/
@Override
public void loadInitialContentsLocked() {
loadDictionaryLocked();
}
/**
* Loads app names to the dictionary.
*/
private void loadDictionaryLocked() {
for (final String name : mAppsManager.getNames()) {
addNameLocked(name);
}
}
/**
* Adds the words in an app label to the binary dictionary along with their n-grams.
*/
private void addNameLocked(final String appLabel) {
NgramContext ngramContext = NgramContext.getEmptyPrevWordsContext(
BinaryDictionary.MAX_PREV_WORD_COUNT_FOR_N_GRAM);
// TODO: Better tokenization for non-Latin writing systems
for (final String word : new LatinTokens(appLabel)) {
if (DEBUG_DUMP) {
Log.d(TAG, "addName word = " + word);
}
final int wordLen = StringUtils.codePointCount(word);
// Don't add single letter words, possibly confuses capitalization of i.
if (1 < wordLen && wordLen <= MAX_WORD_LENGTH) {
if (DEBUG) {
Log.d(TAG, "addName " + appLabel + ", " + word + ", " + ngramContext);
}
runGCIfRequiredLocked(true /* mindsBlockByGC */);
addUnigramLocked(word, AppsDictionaryConstantsKt.FREQUENCY_FOR_APPS,
null /* shortcut */, 0 /* shortcutFreq */, false /* isNotAWord */,
false /* isPossiblyOffensive */,
BinaryDictionary.NOT_A_VALID_TIMESTAMP);
if (ngramContext.isValid()) {
runGCIfRequiredLocked(true /* mindsBlockByGC */);
addNgramEntryLocked(ngramContext,
word,
AppsDictionaryConstantsKt.FREQUENCY_FOR_APPS_BIGRAM,
BinaryDictionary.NOT_A_VALID_TIMESTAMP);
}
ngramContext = ngramContext.getNextNgramContext(
new NgramContext.WordInfo(word));
}
}
}
}

View file

@ -0,0 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-only
package helium314.keyboard.latin
/**
* Frequency for app names into the dictionary
*/
const val FREQUENCY_FOR_APPS: Int = 40
const val FREQUENCY_FOR_APPS_BIGRAM: Int = 90

View file

@ -0,0 +1,30 @@
// SPDX-License-Identifier: GPL-3.0-only
package helium314.keyboard.latin
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import java.util.HashSet
class AppsManager(context: Context) {
private val mPackageManager: PackageManager = context.packageManager
/**
* Returns all app labels associated with a launcher icon, sorted arbitrarily.
*/
fun getNames(): HashSet<String> {
val filter = Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER)
// activities with an entry/icon for the launcher
val launcherApps: List<ResolveInfo> = mPackageManager.queryIntentActivities(filter, 0)
val names = HashSet<String>(launcherApps.size)
for (info in launcherApps) {
val name = info.activityInfo.loadLabel(mPackageManager).toString()
names.add(name)
}
return names
}
}

View file

@ -48,6 +48,7 @@ public abstract class Dictionary {
// phony dictionary instances for them.
public static final String TYPE_MAIN = "main";
public static final String TYPE_CONTACTS = "contacts";
public static final String TYPE_APPS = "apps";
// User dictionary, the system-managed one.
public static final String TYPE_USER = "user";
// User history dictionary internal to LatinIME.
@ -173,6 +174,7 @@ public abstract class Dictionary {
case TYPE_USER_TYPED,
TYPE_USER,
TYPE_CONTACTS,
TYPE_APPS,
TYPE_USER_HISTORY -> true;
default -> false;
};

View file

@ -32,11 +32,13 @@ public interface DictionaryFacilitator {
String[] ALL_DICTIONARY_TYPES = new String[] {
Dictionary.TYPE_MAIN,
Dictionary.TYPE_CONTACTS,
Dictionary.TYPE_APPS,
Dictionary.TYPE_USER_HISTORY,
Dictionary.TYPE_USER};
String[] DYNAMIC_DICTIONARY_TYPES = new String[] {
Dictionary.TYPE_CONTACTS,
Dictionary.TYPE_APPS,
Dictionary.TYPE_USER_HISTORY,
Dictionary.TYPE_USER};
@ -87,6 +89,7 @@ public interface DictionaryFacilitator {
boolean usesSameSettings(
@NonNull final List<Locale> locales,
final boolean contacts,
final boolean apps,
final boolean personalization,
@Nullable final String account
);
@ -97,6 +100,7 @@ public interface DictionaryFacilitator {
final Context context,
final Locale newLocale,
final boolean useContactsDict,
final boolean useAppsDict,
final boolean usePersonalizedDicts,
final boolean forceReloadMainDictionary,
@Nullable final String account,

View file

@ -16,6 +16,23 @@ import android.view.inputmethod.InputMethodSubtype;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
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;
import java.util.Scanner;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import helium314.keyboard.keyboard.Keyboard;
import helium314.keyboard.latin.NgramContext.WordInfo;
import helium314.keyboard.latin.SuggestedWords.SuggestedWordInfo;
@ -35,23 +52,6 @@ import helium314.keyboard.latin.utils.SubtypeSettings;
import helium314.keyboard.latin.utils.SubtypeUtilsKt;
import helium314.keyboard.latin.utils.SuggestionResults;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
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;
import java.util.Scanner;
import java.util.Set;
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),
@ -274,6 +274,10 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
return mDictionaryGroups.get(0).getSubDict(Dictionary.TYPE_CONTACTS) != null;
}
public boolean usesApps() {
return mDictionaryGroups.get(0).getSubDict(Dictionary.TYPE_APPS) != null;
}
public boolean usesPersonalization() {
return mDictionaryGroups.get(0).getSubDict(Dictionary.TYPE_USER_HISTORY) != null;
}
@ -306,6 +310,7 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
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);
case Dictionary.TYPE_APPS -> AppsBinaryDictionary.getDictionary(context, locale, dictFile, dictNamePrefix, account);
default -> null;
};
} catch (final SecurityException | IllegalArgumentException e) {
@ -332,6 +337,7 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
final Context context,
@NonNull final Locale newLocale,
final boolean useContactsDict,
final boolean useAppsDict,
final boolean usePersonalizedDicts,
final boolean forceReloadMainDictionary,
@Nullable final String account,
@ -364,6 +370,9 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
&& PermissionsUtil.checkAllPermissionsGranted(context, Manifest.permission.READ_CONTACTS)) {
subDictTypesToUse.add(Dictionary.TYPE_CONTACTS);
}
if (useAppsDict) {
subDictTypesToUse.add(Dictionary.TYPE_APPS);
}
if (usePersonalizedDicts) {
subDictTypesToUse.add(Dictionary.TYPE_USER_HISTORY);
}
@ -958,6 +967,7 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
if (historyDict != null) {
historyDict.removeUnigramEntryDynamically(word);
}
// and from personal dictionary
final ExpandableBinaryDictionary userDict = group.getSubDict(Dictionary.TYPE_USER);
if (userDict != null) {
@ -965,19 +975,28 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
}
final ExpandableBinaryDictionary contactsDict = group.getSubDict(Dictionary.TYPE_CONTACTS);
if (contactsDict != null) {
if (contactsDict.isInDictionary(word)) {
contactsDict.removeUnigramEntryDynamically(word); // will be gone until next reload of dict
addToBlacklist(word, group);
return;
}
}
if (!group.hasDict(Dictionary.TYPE_MAIN, null))
if (contactsDict != null && contactsDict.isInDictionary(word)) {
contactsDict.removeUnigramEntryDynamically(word); // will be gone until next reload of dict
addToBlacklist(word, group);
return;
}
final ExpandableBinaryDictionary appsDict = group.getSubDict(Dictionary.TYPE_APPS);
if (appsDict != null && appsDict.isInDictionary(word)) {
appsDict.removeUnigramEntryDynamically(word); // will be gone until next reload of dict
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);

View file

@ -26,6 +26,7 @@ public class DictionaryFacilitatorLruCache {
private final Object mLock = new Object();
private final DictionaryFacilitator mDictionaryFacilitator;
private boolean mUseContactsDictionary;
private boolean mUseAppsDictionary;
private Locale mLocale;
public DictionaryFacilitatorLruCache(final Context context, final String dictionaryNamePrefix) {
@ -59,7 +60,7 @@ public class DictionaryFacilitatorLruCache {
if (mLocale != null) {
// Note: Given that personalized dictionaries are not used here; we can pass null account.
mDictionaryFacilitator.resetDictionaries(mContext, mLocale,
mUseContactsDictionary, false /* usePersonalizedDicts */,
mUseContactsDictionary, mUseAppsDictionary, false /* usePersonalizedDicts */,
false /* forceReloadMainDictionary */, null /* account */,
mDictionaryNamePrefix, null /* listener */);
}
@ -77,6 +78,18 @@ public class DictionaryFacilitatorLruCache {
}
}
public void setUseAppsDictionary(final boolean useAppsDictionary) {
synchronized (mLock) {
if (mUseAppsDictionary == useAppsDictionary) {
// The value has not been changed.
return;
}
mUseAppsDictionary = useAppsDictionary;
resetDictionariesForLocaleLocked();
waitForLoadingMainDictionary(mDictionaryFacilitator);
}
}
public DictionaryFacilitator get(final Locale locale) {
synchronized (mLock) {
if (!mDictionaryFacilitator.isForLocale(locale)) {

View file

@ -668,6 +668,7 @@ public class LatinIME extends InputMethodService implements
if (mDictionaryFacilitator.usesSameSettings(
locales,
mSettings.getCurrent().mUseContactsDictionary,
mSettings.getCurrent().mUseAppsDictionary,
mSettings.getCurrent().mUsePersonalizedDicts,
mSettings.getCurrent().mAccount
)) {
@ -686,8 +687,8 @@ public class LatinIME extends InputMethodService implements
private void resetDictionaryFacilitator(@NonNull final Locale locale) {
final SettingsValues settingsValues = mSettings.getCurrent();
mDictionaryFacilitator.resetDictionaries(this, locale,
settingsValues.mUseContactsDictionary, settingsValues.mUsePersonalizedDicts,
false, settingsValues.mAccount, "", this);
settingsValues.mUseContactsDictionary, settingsValues.mUseAppsDictionary,
settingsValues.mUsePersonalizedDicts, false, settingsValues.mAccount, "", this);
mInputLogic.mSuggest.setAutoCorrectionThreshold(settingsValues.mAutoCorrectionThreshold);
}
@ -698,7 +699,7 @@ public class LatinIME extends InputMethodService implements
final SettingsValues settingsValues = mSettings.getCurrent();
mDictionaryFacilitator.resetDictionaries(this /* context */,
mDictionaryFacilitator.getMainLocale(), settingsValues.mUseContactsDictionary,
settingsValues.mUsePersonalizedDicts,
settingsValues.mUseAppsDictionary, settingsValues.mUsePersonalizedDicts,
true /* forceReloadMainDictionary */,
settingsValues.mAccount, "" /* dictNamePrefix */,
this /* DictionaryInitializationListener */);

View file

@ -0,0 +1,38 @@
// SPDX-License-Identifier: GPL-3.0-only
package helium314.keyboard.latin
/**
* Tokenizes strings by groupings of non-space characters, making them iterable. Note that letters,
* punctuations, etc. are all treated the same by this construct.
*/
class LatinTokens(phrase: String) : Iterable<String> {
private val mPhrase = phrase
private val mLength = phrase.length
private val mStartPos = phrase.indexOfFirst { !Character.isWhitespace(it) }
// the iterator should start at the first non-whitespace character
override fun iterator() = object : Iterator<String> {
private var startPos = mStartPos
override fun hasNext(): Boolean {
return startPos < mLength
}
override fun next(): String {
var endPos = startPos
do if (++endPos >= mLength) break
while (!Character.isWhitespace(mPhrase[endPos]))
val word = mPhrase.substring(startPos, endPos)
if (endPos < mLength) {
do if (++endPos >= mLength) break
while (Character.isWhitespace(mPhrase[endPos]))
}
startPos = endPos
return word
}
}
}

View file

@ -110,6 +110,7 @@ object Defaults {
const val PREF_GESTURE_TRAIL_FADEOUT_DURATION = 800
const val PREF_SHOW_SETUP_WIZARD_ICON = true
const val PREF_USE_CONTACTS = false
const val PREF_USE_APPS = false
const val PREFS_LONG_PRESS_SYMBOLS_FOR_NUMPAD = false
const val PREF_ONE_HANDED_MODE = false
@SuppressLint("RtlHardcoded")

View file

@ -114,6 +114,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
public static final String PREF_GESTURE_TRAIL_FADEOUT_DURATION = "gesture_trail_fadeout_duration";
public static final String PREF_SHOW_SETUP_WIZARD_ICON = "show_setup_wizard_icon";
public static final String PREF_USE_CONTACTS = "use_contacts";
public static final String PREF_USE_APPS = "use_apps";
public static final String PREFS_LONG_PRESS_SYMBOLS_FOR_NUMPAD = "long_press_symbols_for_numpad";
// one-handed mode gravity, enablement and scale, stored separately per orientation

View file

@ -103,6 +103,7 @@ public class SettingsValues {
public final int mScreenMetrics;
public final boolean mAddToPersonalDictionary;
public final boolean mUseContactsDictionary;
public final boolean mUseAppsDictionary;
public final boolean mCustomNavBarColor;
public final float mKeyboardHeightScale;
public final boolean mUrlDetectionEnabled;
@ -251,6 +252,7 @@ public class SettingsValues {
mPopupKeyLabelSources = SubtypeUtilsKt.getPopupKeyLabelSources(selectedSubtype, prefs);
mAddToPersonalDictionary = prefs.getBoolean(Settings.PREF_ADD_TO_PERSONAL_DICTIONARY, Defaults.PREF_ADD_TO_PERSONAL_DICTIONARY);
mUseContactsDictionary = SettingsValues.readUseContactsEnabled(prefs, context);
mUseAppsDictionary = prefs.getBoolean(Settings.PREF_USE_APPS, Defaults.PREF_USE_APPS);
mCustomNavBarColor = prefs.getBoolean(Settings.PREF_NAVBAR_COLOR, Defaults.PREF_NAVBAR_COLOR);
mNarrowKeyGaps = prefs.getBoolean(Settings.PREF_NARROW_KEY_GAPS, Defaults.PREF_NARROW_KEY_GAPS);
mSettingsValuesForSuggestion = new SettingsValuesForSuggestion(
@ -334,11 +336,12 @@ public class SettingsValues {
return mDisplayOrientation == configuration.orientation;
}
private static boolean readUseContactsEnabled(final SharedPreferences prefs, final Context context) {
private static boolean readUseContactsEnabled(final SharedPreferences prefs, final Context ctx) {
final boolean setting = prefs.getBoolean(Settings.PREF_USE_CONTACTS, Defaults.PREF_USE_CONTACTS);
if (!setting) return false;
if (PermissionsUtil.checkAllPermissionsGranted(context, Manifest.permission.READ_CONTACTS))
if (PermissionsUtil.checkAllPermissionsGranted(ctx, Manifest.permission.READ_CONTACTS)) {
return true;
}
// disable if permission not granted
prefs.edit().putBoolean(Settings.PREF_USE_CONTACTS, false).apply();
return false;

View file

@ -83,6 +83,7 @@ public final class AndroidSpellCheckerService extends SpellCheckerService
final SharedPreferences prefs = KtxKt.prefs(this);
prefs.registerOnSharedPreferenceChangeListener(this);
onSharedPreferenceChanged(prefs, Settings.PREF_USE_CONTACTS);
onSharedPreferenceChanged(prefs, Settings.PREF_USE_APPS);
final boolean blockOffensive = prefs.getBoolean(Settings.PREF_BLOCK_POTENTIALLY_OFFENSIVE, Defaults.PREF_BLOCK_POTENTIALLY_OFFENSIVE);
mSettingsValuesForSuggestion = new SettingsValuesForSuggestion(blockOffensive, false);
}
@ -93,13 +94,19 @@ public final class AndroidSpellCheckerService extends SpellCheckerService
@Override
public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
if (Settings.PREF_USE_CONTACTS.equals(key)) {
if (key != null) switch (key) {
case Settings.PREF_USE_CONTACTS -> {
final boolean useContactsDictionary = prefs.getBoolean(Settings.PREF_USE_CONTACTS, Defaults.PREF_USE_CONTACTS);
mDictionaryFacilitatorCache.setUseContactsDictionary(useContactsDictionary);
} else if (Settings.PREF_BLOCK_POTENTIALLY_OFFENSIVE.equals(key)) {
}
case Settings.PREF_USE_APPS -> {
final boolean useAppsDictionary = prefs.getBoolean(Settings.PREF_USE_APPS, Defaults.PREF_USE_APPS);
mDictionaryFacilitatorCache.setUseAppsDictionary(useAppsDictionary);
}
case Settings.PREF_BLOCK_POTENTIALLY_OFFENSIVE -> {
final boolean blockOffensive = prefs.getBoolean(Settings.PREF_BLOCK_POTENTIALLY_OFFENSIVE, Defaults.PREF_BLOCK_POTENTIALLY_OFFENSIVE);
mSettingsValuesForSuggestion = new SettingsValuesForSuggestion(blockOffensive, false);
}
}}
}
@Override

View file

@ -99,6 +99,7 @@ class SettingsActivity : ComponentActivity(), SharedPreferences.OnSharedPreferen
if (spellchecker)
Column { // lazy way of implementing spell checker settings
settingsContainer[Settings.PREF_USE_CONTACTS]!!.Preference()
settingsContainer[Settings.PREF_USE_APPS]!!.Preference()
settingsContainer[Settings.PREF_BLOCK_POTENTIALLY_OFFENSIVE]!!.Preference()
}
else

View file

@ -68,6 +68,7 @@ fun TextCorrectionScreen(
Settings.PREF_BIGRAM_PREDICTIONS,
Settings.PREF_SUGGEST_CLIPBOARD_CONTENT,
Settings.PREF_USE_CONTACTS,
Settings.PREF_USE_APPS,
if (prefs.getBoolean(Settings.PREF_KEY_USE_PERSONALIZED_DICTS, Defaults.PREF_KEY_USE_PERSONALIZED_DICTS))
Settings.PREF_ADD_TO_PERSONAL_DICTIONARY else null
)
@ -195,6 +196,11 @@ fun createCorrectionSettings(context: Context) = listOf(
}
)
},
Setting(context, Settings.PREF_USE_APPS,
R.string.use_apps_dict, R.string.use_apps_dict_summary
) { setting ->
SwitchPreference(setting, Defaults.PREF_USE_APPS)
},
Setting(context, Settings.PREF_ADD_TO_PERSONAL_DICTIONARY,
R.string.add_to_personal_dictionary, R.string.add_to_personal_dictionary_summary
) {

View file

@ -76,6 +76,10 @@
<string name="use_contacts_dict">Look up contact names</string>
<!-- Description for option enabling or disabling the use of names of people in Contacts for suggestion and correction -->
<string name="use_contacts_dict_summary">Use names from Contacts for suggestions and corrections</string>
<!-- Option name for enabling or disabling the use of installed app names for suggestion and correction -->
<string name="use_apps_dict">Look up app names</string>
<!-- Description for option enabling or disabling the use of installed app names for suggestion and correction -->
<string name="use_apps_dict_summary">Use names of installed apps for suggestions and corrections</string>
<!-- Option name for enabling the use by the keyboards of sent/received messages, e-mail and typing history to improve suggestion accuracy -->
<string name="use_personalized_dicts">Personalized suggestions</string>
<!-- Dialog message informing that dictionaries will be deleted on changing the setting -->