mirror of
https://github.com/Helium314/HeliBoard.git
synced 2025-06-10 00:27:45 +00:00
add option to enable phrase gesture
This commit is contained in:
parent
52d50a6de6
commit
eebaa2881b
10 changed files with 30 additions and 659 deletions
|
@ -294,8 +294,9 @@ public final class BinaryDictionary extends Dictionary {
|
||||||
}
|
}
|
||||||
session.mNativeSuggestOptions.setUseFullEditDistance(mUseFullEditDistance);
|
session.mNativeSuggestOptions.setUseFullEditDistance(mUseFullEditDistance);
|
||||||
session.mNativeSuggestOptions.setIsGesture(isGesture);
|
session.mNativeSuggestOptions.setIsGesture(isGesture);
|
||||||
session.mNativeSuggestOptions.setBlockOffensiveWords(
|
if (isGesture)
|
||||||
settingsValuesForSuggestion.mBlockPotentiallyOffensive);
|
session.mNativeSuggestOptions.setIsSpaceAwareGesture(settingsValuesForSuggestion.mSpaceAwareGesture);
|
||||||
|
session.mNativeSuggestOptions.setBlockOffensiveWords(settingsValuesForSuggestion.mBlockPotentiallyOffensive);
|
||||||
session.mNativeSuggestOptions.setWeightForLocale(weightForLocale);
|
session.mNativeSuggestOptions.setWeightForLocale(weightForLocale);
|
||||||
if (inOutWeightOfLangModelVsSpatialModel != null) {
|
if (inOutWeightOfLangModelVsSpatialModel != null) {
|
||||||
session.mInputOutputWeightOfLangModelVsSpatialModel[0] =
|
session.mInputOutputWeightOfLangModelVsSpatialModel[0] =
|
||||||
|
|
|
@ -1,651 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (C) 2015 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.dslul.openboard.inputmethod.latin;
|
|
||||||
|
|
||||||
import android.content.ContentResolver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.database.ContentObserver;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.provider.UserDictionary;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import org.dslul.openboard.inputmethod.annotations.UsedForTesting;
|
|
||||||
import org.dslul.openboard.inputmethod.latin.common.CollectionUtils;
|
|
||||||
import org.dslul.openboard.inputmethod.latin.common.LocaleUtils;
|
|
||||||
import org.dslul.openboard.inputmethod.latin.define.DebugFlags;
|
|
||||||
import org.dslul.openboard.inputmethod.latin.utils.ExecutorUtils;
|
|
||||||
|
|
||||||
import java.io.Closeable;
|
|
||||||
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.Set;
|
|
||||||
import java.util.concurrent.ScheduledFuture;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class provides the ability to look into the system-wide "Personal dictionary". It loads the
|
|
||||||
* data once when created and reloads it when notified of changes to {@link UserDictionary}
|
|
||||||
*
|
|
||||||
* It can be used directly to validate words or expand shortcuts, and it can be used by instances
|
|
||||||
* of {@link PersonalLanguageModelHelper} that create language model files for a specific input
|
|
||||||
* locale.
|
|
||||||
*
|
|
||||||
* Note, that the initial dictionary loading happens asynchronously so it is possible (hopefully
|
|
||||||
* rarely) that {@link #isValidWord} or {@link #expandShortcut} is called before the initial load
|
|
||||||
* has started.
|
|
||||||
*
|
|
||||||
* The caller should explicitly call {@link #close} when the object is no longer needed, in order
|
|
||||||
* to release any resources and references to this object. A service should create this object in
|
|
||||||
* {@link android.app.Service#onCreate} and close it in {@link android.app.Service#onDestroy}.
|
|
||||||
*/
|
|
||||||
public class PersonalDictionaryLookup implements Closeable {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* To avoid loading too many dictionary entries in memory, we cap them at this number. If
|
|
||||||
* that number is exceeded, the lowest-frequency items will be dropped. Note, there is no
|
|
||||||
* explicit cap on the number of locales in every entry.
|
|
||||||
*/
|
|
||||||
private static final int MAX_NUM_ENTRIES = 1000;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The delay (in milliseconds) to impose on reloads. Previously scheduled reloads will be
|
|
||||||
* cancelled if a new reload is scheduled before the delay expires. Thus, only the last
|
|
||||||
* reload in the series of frequent reloads will execute.
|
|
||||||
*
|
|
||||||
* Note, this value should be low enough to allow the "Add to dictionary" feature in the
|
|
||||||
* TextView correction (red underline) drop-down menu to work properly in the following case:
|
|
||||||
*
|
|
||||||
* 1. User types OOV (out-of-vocabulary) word.
|
|
||||||
* 2. The OOV is red-underlined.
|
|
||||||
* 3. User selects "Add to dictionary". The red underline disappears while the OOV is
|
|
||||||
* in a composing span.
|
|
||||||
* 4. The user taps space. The red underline should NOT reappear. If this value is very
|
|
||||||
* high and the user performs the space tap fast enough, the red underline may reappear.
|
|
||||||
*/
|
|
||||||
@UsedForTesting
|
|
||||||
static final int RELOAD_DELAY_MS = 200;
|
|
||||||
|
|
||||||
@UsedForTesting
|
|
||||||
static final Locale ANY_LOCALE = new Locale("");
|
|
||||||
|
|
||||||
private final String mTag;
|
|
||||||
private final ContentResolver mResolver;
|
|
||||||
private final String mServiceName;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface to implement for classes interested in getting notified of updates.
|
|
||||||
*/
|
|
||||||
public interface PersonalDictionaryListener {
|
|
||||||
void onUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
private final Set<PersonalDictionaryListener> mListeners = new HashSet<>();
|
|
||||||
|
|
||||||
public void addListener(@NonNull final PersonalDictionaryListener listener) {
|
|
||||||
mListeners.add(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void removeListener(@NonNull final PersonalDictionaryListener listener) {
|
|
||||||
mListeners.remove(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Broadcast the update to all the Locale-specific language models.
|
|
||||||
*/
|
|
||||||
@UsedForTesting
|
|
||||||
void notifyListeners() {
|
|
||||||
for (PersonalDictionaryListener listener : mListeners) {
|
|
||||||
listener.onUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Content observer for changes to the personal dictionary. It has the following properties:
|
|
||||||
* 1. It spawns off a reload in another thread, after some delay.
|
|
||||||
* 2. It cancels previously scheduled reloads, and only executes the latest.
|
|
||||||
* 3. It may be called multiple times quickly in succession (and is in fact called so
|
|
||||||
* when the dictionary is edited through its settings UI, when sometimes multiple
|
|
||||||
* notifications are sent for the edited entry, but also for the entire dictionary).
|
|
||||||
*/
|
|
||||||
private class PersonalDictionaryContentObserver extends ContentObserver implements Runnable {
|
|
||||||
public PersonalDictionaryContentObserver() {
|
|
||||||
super(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean deliverSelfNotifications() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Support pre-API16 platforms.
|
|
||||||
@Override
|
|
||||||
public void onChange(boolean selfChange) {
|
|
||||||
onChange(selfChange, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onChange(boolean selfChange, Uri uri) {
|
|
||||||
if (DebugFlags.DEBUG_ENABLED) {
|
|
||||||
Log.d(mTag, "onChange() : URI = " + uri);
|
|
||||||
}
|
|
||||||
// Cancel (but don't interrupt) any pending reloads (except the initial load).
|
|
||||||
if (mReloadFuture != null && !mReloadFuture.isCancelled() &&
|
|
||||||
!mReloadFuture.isDone()) {
|
|
||||||
// Note, that if already cancelled or done, this will do nothing.
|
|
||||||
boolean isCancelled = mReloadFuture.cancel(false);
|
|
||||||
if (DebugFlags.DEBUG_ENABLED) {
|
|
||||||
if (isCancelled) {
|
|
||||||
Log.d(mTag, "onChange() : Canceled previous reload request");
|
|
||||||
} else {
|
|
||||||
Log.d(mTag, "onChange() : Failed to cancel previous reload request");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (DebugFlags.DEBUG_ENABLED) {
|
|
||||||
Log.d(mTag, "onChange() : Scheduling reload in " + RELOAD_DELAY_MS + " ms");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Schedule a new reload after RELOAD_DELAY_MS.
|
|
||||||
mReloadFuture = ExecutorUtils.getBackgroundExecutor(mServiceName)
|
|
||||||
.schedule(this, RELOAD_DELAY_MS, TimeUnit.MILLISECONDS);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
loadPersonalDictionary();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final PersonalDictionaryContentObserver mPersonalDictionaryContentObserver =
|
|
||||||
new PersonalDictionaryContentObserver();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates that a load is in progress, so no need for another.
|
|
||||||
*/
|
|
||||||
private AtomicBoolean mIsLoading = new AtomicBoolean(false);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates that this lookup object has been close()d.
|
|
||||||
*/
|
|
||||||
private AtomicBoolean mIsClosed = new AtomicBoolean(false);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We store a map from a dictionary word to the set of locales & raw string(as it appears)
|
|
||||||
* We then iterate over the set of locales to find a match using LocaleUtils.
|
|
||||||
*/
|
|
||||||
private volatile HashMap<String, HashMap<Locale, String>> mDictWords;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We store a map from a shortcut to a word for each locale.
|
|
||||||
* Shortcuts that apply to any locale are keyed by {@link #ANY_LOCALE}.
|
|
||||||
*/
|
|
||||||
private volatile HashMap<Locale, HashMap<String, String>> mShortcutsPerLocale;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The last-scheduled reload future. Saved in order to cancel a pending reload if a new one
|
|
||||||
* is coming.
|
|
||||||
*/
|
|
||||||
private volatile ScheduledFuture<?> mReloadFuture;
|
|
||||||
|
|
||||||
private volatile List<DictionaryStats> mDictionaryStats;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param context the context from which to obtain content resolver
|
|
||||||
*/
|
|
||||||
public PersonalDictionaryLookup(
|
|
||||||
@NonNull final Context context,
|
|
||||||
@NonNull final String serviceName) {
|
|
||||||
mTag = serviceName + ".Personal";
|
|
||||||
|
|
||||||
Log.i(mTag, "create()");
|
|
||||||
|
|
||||||
mServiceName = serviceName;
|
|
||||||
mDictionaryStats = new ArrayList<DictionaryStats>();
|
|
||||||
mDictionaryStats.add(new DictionaryStats(ANY_LOCALE, Dictionary.TYPE_USER, 0));
|
|
||||||
mDictionaryStats.add(new DictionaryStats(ANY_LOCALE, Dictionary.TYPE_USER_SHORTCUT, 0));
|
|
||||||
|
|
||||||
// Obtain a content resolver.
|
|
||||||
mResolver = context.getContentResolver();
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<DictionaryStats> getDictionaryStats() {
|
|
||||||
return mDictionaryStats;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void open() {
|
|
||||||
Log.i(mTag, "open()");
|
|
||||||
|
|
||||||
// Schedule the initial load to run immediately. It's possible that the first call to
|
|
||||||
// isValidWord occurs before the dictionary has actually loaded, so it should not
|
|
||||||
// assume that the dictionary has been loaded.
|
|
||||||
loadPersonalDictionary();
|
|
||||||
|
|
||||||
// Register the observer to be notified on changes to the personal dictionary and all
|
|
||||||
// individual items.
|
|
||||||
//
|
|
||||||
// If the user is interacting with the Personal Dictionary settings UI, or with the
|
|
||||||
// "Add to dictionary" drop-down option, duplicate notifications will be sent for the same
|
|
||||||
// edit: if a new entry is added, there is a notification for the entry itself, and
|
|
||||||
// separately for the entire dictionary. However, when used programmatically,
|
|
||||||
// only notifications for the specific edits are sent. Thus, the observer is registered to
|
|
||||||
// receive every possible notification, and instead has throttling logic to avoid doing too
|
|
||||||
// many reloads.
|
|
||||||
mResolver.registerContentObserver(
|
|
||||||
UserDictionary.Words.CONTENT_URI,
|
|
||||||
true /* notifyForDescendents */,
|
|
||||||
mPersonalDictionaryContentObserver);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* To be called by the garbage collector in the off chance that the service did not clean up
|
|
||||||
* properly. Do not rely on this getting called, and make sure close() is called explicitly.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void finalize() throws Throwable {
|
|
||||||
try {
|
|
||||||
if (DebugFlags.DEBUG_ENABLED) {
|
|
||||||
Log.d(mTag, "finalize()");
|
|
||||||
}
|
|
||||||
close();
|
|
||||||
} finally {
|
|
||||||
super.finalize();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cleans up PersonalDictionaryLookup: shuts down any extra threads and unregisters the observer.
|
|
||||||
*
|
|
||||||
* It is safe, but not advised to call this multiple times, and isValidWord would continue to
|
|
||||||
* work, but no data will be reloaded any longer.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void close() {
|
|
||||||
if (DebugFlags.DEBUG_ENABLED) {
|
|
||||||
Log.d(mTag, "close() : Unregistering content observer");
|
|
||||||
}
|
|
||||||
if (mIsClosed.compareAndSet(false, true)) {
|
|
||||||
// Unregister the content observer.
|
|
||||||
mResolver.unregisterContentObserver(mPersonalDictionaryContentObserver);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the initial load has been performed.
|
|
||||||
*
|
|
||||||
* @return true if the initial load is successful
|
|
||||||
*/
|
|
||||||
public boolean isLoaded() {
|
|
||||||
return mDictWords != null && mShortcutsPerLocale != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the set of words defined for the given locale and more general locales.
|
|
||||||
*
|
|
||||||
* For example, input locale en_US uses data for en_US, en, and the global dictionary.
|
|
||||||
*
|
|
||||||
* Note that this method returns expanded words, not shortcuts. Shortcuts are handled
|
|
||||||
* by {@link #getShortcutsForLocale}.
|
|
||||||
*
|
|
||||||
* @param inputLocale the locale to restrict for
|
|
||||||
* @return set of words that apply to the given locale.
|
|
||||||
*/
|
|
||||||
public Set<String> getWordsForLocale(@NonNull final Locale inputLocale) {
|
|
||||||
final HashMap<String, HashMap<Locale, String>> dictWords = mDictWords;
|
|
||||||
if (CollectionUtils.isNullOrEmpty(dictWords)) {
|
|
||||||
return Collections.emptySet();
|
|
||||||
}
|
|
||||||
|
|
||||||
final Set<String> words = new HashSet<>();
|
|
||||||
final String inputLocaleString = inputLocale.toString();
|
|
||||||
for (String word : dictWords.keySet()) {
|
|
||||||
HashMap<Locale, String> localeStringMap = dictWords.get(word);
|
|
||||||
if (!CollectionUtils.isNullOrEmpty(localeStringMap)) {
|
|
||||||
for (Locale wordLocale : localeStringMap.keySet()) {
|
|
||||||
final String wordLocaleString = wordLocale.toString();
|
|
||||||
final int match = LocaleUtils.getMatchLevel(wordLocaleString, inputLocaleString);
|
|
||||||
if (LocaleUtils.isMatch(match)) {
|
|
||||||
words.add(localeStringMap.get(wordLocale));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return words;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the set of shortcuts defined for the given locale and more general locales.
|
|
||||||
*
|
|
||||||
* For example, input locale en_US uses data for en_US, en, and the global dictionary.
|
|
||||||
*
|
|
||||||
* Note that this method returns shortcut keys, not expanded words. Words are handled
|
|
||||||
* by {@link #getWordsForLocale}.
|
|
||||||
*
|
|
||||||
* @param inputLocale the locale to restrict for
|
|
||||||
* @return set of shortcuts that apply to the given locale.
|
|
||||||
*/
|
|
||||||
public Set<String> getShortcutsForLocale(@NonNull final Locale inputLocale) {
|
|
||||||
final Map<Locale, HashMap<String, String>> shortcutsPerLocale = mShortcutsPerLocale;
|
|
||||||
if (CollectionUtils.isNullOrEmpty(shortcutsPerLocale)) {
|
|
||||||
return Collections.emptySet();
|
|
||||||
}
|
|
||||||
|
|
||||||
final Set<String> shortcuts = new HashSet<>();
|
|
||||||
if (!TextUtils.isEmpty(inputLocale.getCountry())) {
|
|
||||||
// First look for the country-specific shortcut: en_US, en_UK, fr_FR, etc.
|
|
||||||
final Map<String, String> countryShortcuts = shortcutsPerLocale.get(inputLocale);
|
|
||||||
if (!CollectionUtils.isNullOrEmpty(countryShortcuts)) {
|
|
||||||
shortcuts.addAll(countryShortcuts.keySet());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next look for the language-specific shortcut: en, fr, etc.
|
|
||||||
final Locale languageOnlyLocale =
|
|
||||||
LocaleUtils.constructLocaleFromString(inputLocale.getLanguage());
|
|
||||||
final Map<String, String> languageShortcuts = shortcutsPerLocale.get(languageOnlyLocale);
|
|
||||||
if (!CollectionUtils.isNullOrEmpty(languageShortcuts)) {
|
|
||||||
shortcuts.addAll(languageShortcuts.keySet());
|
|
||||||
}
|
|
||||||
|
|
||||||
// If all else fails, look for a global shortcut.
|
|
||||||
final Map<String, String> globalShortcuts = shortcutsPerLocale.get(ANY_LOCALE);
|
|
||||||
if (!CollectionUtils.isNullOrEmpty(globalShortcuts)) {
|
|
||||||
shortcuts.addAll(globalShortcuts.keySet());
|
|
||||||
}
|
|
||||||
|
|
||||||
return shortcuts;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines if the given word is a valid word in the given locale based on the dictionary.
|
|
||||||
* It tries hard to find a match: for example, casing is ignored and if the word is present in a
|
|
||||||
* more general locale (e.g. en or all locales), and isValidWord is asking for a more specific
|
|
||||||
* locale (e.g. en_US), it will be considered a match.
|
|
||||||
*
|
|
||||||
* @param word the word to match
|
|
||||||
* @param inputLocale the locale in which to match the word
|
|
||||||
* @return true iff the word has been matched for this locale in the dictionary.
|
|
||||||
*/
|
|
||||||
public boolean isValidWord(@NonNull final String word, @NonNull final Locale inputLocale) {
|
|
||||||
if (!isLoaded()) {
|
|
||||||
// This is a corner case in the event the initial load of the dictionary has not
|
|
||||||
// completed. In that case, we assume the word is not a valid word in the dictionary.
|
|
||||||
if (DebugFlags.DEBUG_ENABLED) {
|
|
||||||
Log.d(mTag, "isValidWord() : Initial load not complete");
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (DebugFlags.DEBUG_ENABLED) {
|
|
||||||
Log.d(mTag, "isValidWord() : Word [" + word + "] in Locale [" + inputLocale + "]");
|
|
||||||
}
|
|
||||||
// Atomically obtain the current copy of mDictWords;
|
|
||||||
final HashMap<String, HashMap<Locale, String>> dictWords = mDictWords;
|
|
||||||
// Lowercase the word using the given locale. Note, that dictionary
|
|
||||||
// words are lowercased using their locale, and theoretically the
|
|
||||||
// lowercasing between two matching locales may differ. For simplicity
|
|
||||||
// we ignore that possibility.
|
|
||||||
final String lowercased = word.toLowerCase(inputLocale);
|
|
||||||
final HashMap<Locale, String> dictLocales = dictWords.get(lowercased);
|
|
||||||
|
|
||||||
if (CollectionUtils.isNullOrEmpty(dictLocales)) {
|
|
||||||
if (DebugFlags.DEBUG_ENABLED) {
|
|
||||||
Log.d(mTag, "isValidWord() : No entry for word [" + word + "]");
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
if (DebugFlags.DEBUG_ENABLED) {
|
|
||||||
Log.d(mTag, "isValidWord() : Found entry for word [" + word + "]");
|
|
||||||
}
|
|
||||||
// Iterate over the locales this word is in.
|
|
||||||
for (final Locale dictLocale : dictLocales.keySet()) {
|
|
||||||
final int matchLevel = LocaleUtils.getMatchLevel(dictLocale.toString(),
|
|
||||||
inputLocale.toString());
|
|
||||||
if (DebugFlags.DEBUG_ENABLED) {
|
|
||||||
Log.d(mTag, "isValidWord() : MatchLevel for DictLocale [" + dictLocale
|
|
||||||
+ "] and InputLocale [" + inputLocale + "] is " + matchLevel);
|
|
||||||
}
|
|
||||||
if (LocaleUtils.isMatch(matchLevel)) {
|
|
||||||
if (DebugFlags.DEBUG_ENABLED) {
|
|
||||||
Log.d(mTag, "isValidWord() : MatchLevel " + matchLevel + " IS a match");
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (DebugFlags.DEBUG_ENABLED) {
|
|
||||||
Log.d(mTag, "isValidWord() : MatchLevel " + matchLevel + " is NOT a match");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (DebugFlags.DEBUG_ENABLED) {
|
|
||||||
Log.d(mTag, "isValidWord() : False, since none of the locales matched");
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Expands the given shortcut for the given locale.
|
|
||||||
*
|
|
||||||
* @param shortcut the shortcut to expand
|
|
||||||
* @param inputLocale the locale in which to expand the shortcut
|
|
||||||
* @return expanded shortcut iff the word is a shortcut in the dictionary.
|
|
||||||
*/
|
|
||||||
@Nullable public String expandShortcut(
|
|
||||||
@NonNull final String shortcut, @NonNull final Locale inputLocale) {
|
|
||||||
if (DebugFlags.DEBUG_ENABLED) {
|
|
||||||
Log.d(mTag, "expandShortcut() : Shortcut [" + shortcut + "] for [" + inputLocale + "]");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Atomically obtain the current copy of mShortcuts;
|
|
||||||
final HashMap<Locale, HashMap<String, String>> shortcutsPerLocale = mShortcutsPerLocale;
|
|
||||||
|
|
||||||
// Exit as early as possible. Most users don't use shortcuts.
|
|
||||||
if (CollectionUtils.isNullOrEmpty(shortcutsPerLocale)) {
|
|
||||||
if (DebugFlags.DEBUG_ENABLED) {
|
|
||||||
Log.d(mTag, "expandShortcut() : User has no shortcuts");
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TextUtils.isEmpty(inputLocale.getCountry())) {
|
|
||||||
// First look for the country-specific shortcut: en_US, en_UK, fr_FR, etc.
|
|
||||||
final String expansionForCountry = expandShortcut(
|
|
||||||
shortcutsPerLocale, shortcut, inputLocale);
|
|
||||||
if (!TextUtils.isEmpty(expansionForCountry)) {
|
|
||||||
if (DebugFlags.DEBUG_ENABLED) {
|
|
||||||
Log.d(mTag, "expandShortcut() : Country expansion is ["
|
|
||||||
+ expansionForCountry + "]");
|
|
||||||
}
|
|
||||||
return expansionForCountry;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next look for the language-specific shortcut: en, fr, etc.
|
|
||||||
final Locale languageOnlyLocale =
|
|
||||||
LocaleUtils.constructLocaleFromString(inputLocale.getLanguage());
|
|
||||||
final String expansionForLanguage = expandShortcut(
|
|
||||||
shortcutsPerLocale, shortcut, languageOnlyLocale);
|
|
||||||
if (!TextUtils.isEmpty(expansionForLanguage)) {
|
|
||||||
if (DebugFlags.DEBUG_ENABLED) {
|
|
||||||
Log.d(mTag, "expandShortcut() : Language expansion is ["
|
|
||||||
+ expansionForLanguage + "]");
|
|
||||||
}
|
|
||||||
return expansionForLanguage;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If all else fails, look for a global shortcut.
|
|
||||||
final String expansionForGlobal = expandShortcut(shortcutsPerLocale, shortcut, ANY_LOCALE);
|
|
||||||
if (!TextUtils.isEmpty(expansionForGlobal) && DebugFlags.DEBUG_ENABLED) {
|
|
||||||
Log.d(mTag, "expandShortcut() : Global expansion is [" + expansionForGlobal + "]");
|
|
||||||
}
|
|
||||||
return expansionForGlobal;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable private String expandShortcut(
|
|
||||||
@Nullable final HashMap<Locale, HashMap<String, String>> shortcutsPerLocale,
|
|
||||||
@NonNull final String shortcut,
|
|
||||||
@NonNull final Locale locale) {
|
|
||||||
if (CollectionUtils.isNullOrEmpty(shortcutsPerLocale)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final HashMap<String, String> localeShortcuts = shortcutsPerLocale.get(locale);
|
|
||||||
if (CollectionUtils.isNullOrEmpty(localeShortcuts)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return localeShortcuts.get(shortcut);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads the personal dictionary in the current thread.
|
|
||||||
*
|
|
||||||
* Only one reload can happen at a time. If already running, will exit quickly.
|
|
||||||
*/
|
|
||||||
private void loadPersonalDictionary() {
|
|
||||||
// Bail out if already in the process of loading.
|
|
||||||
if (!mIsLoading.compareAndSet(false, true)) {
|
|
||||||
Log.i(mTag, "loadPersonalDictionary() : Already Loading (exit)");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Log.i(mTag, "loadPersonalDictionary() : Start Loading");
|
|
||||||
HashMap<String, HashMap<Locale, String>> dictWords = new HashMap<>();
|
|
||||||
HashMap<Locale, HashMap<String, String>> shortcutsPerLocale = new HashMap<>();
|
|
||||||
// Load the dictionary. Items are returned in the default sort order (by frequency).
|
|
||||||
Cursor cursor = mResolver.query(UserDictionary.Words.CONTENT_URI,
|
|
||||||
null, null, null, UserDictionary.Words.DEFAULT_SORT_ORDER);
|
|
||||||
if (null == cursor || cursor.getCount() < 1) {
|
|
||||||
Log.i(mTag, "loadPersonalDictionary() : Empty");
|
|
||||||
} else {
|
|
||||||
// Iterate over the entries in the personal dictionary. Note, that iteration is in
|
|
||||||
// descending frequency by default.
|
|
||||||
while (dictWords.size() < MAX_NUM_ENTRIES && cursor.moveToNext()) {
|
|
||||||
// If there is no column for locale, skip this entry. An empty
|
|
||||||
// locale on the other hand will not be skipped.
|
|
||||||
final int dictLocaleIndex = cursor.getColumnIndex(UserDictionary.Words.LOCALE);
|
|
||||||
if (dictLocaleIndex < 0) {
|
|
||||||
if (DebugFlags.DEBUG_ENABLED) {
|
|
||||||
Log.d(mTag, "loadPersonalDictionary() : Entry without LOCALE, skipping");
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// If there is no column for word, skip this entry.
|
|
||||||
final int dictWordIndex = cursor.getColumnIndex(UserDictionary.Words.WORD);
|
|
||||||
if (dictWordIndex < 0) {
|
|
||||||
if (DebugFlags.DEBUG_ENABLED) {
|
|
||||||
Log.d(mTag, "loadPersonalDictionary() : Entry without WORD, skipping");
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// If the word is null, skip this entry.
|
|
||||||
final String rawDictWord = cursor.getString(dictWordIndex);
|
|
||||||
if (null == rawDictWord) {
|
|
||||||
if (DebugFlags.DEBUG_ENABLED) {
|
|
||||||
Log.d(mTag, "loadPersonalDictionary() : Null word");
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// If the locale is null, that's interpreted to mean all locales. Note, the special
|
|
||||||
// zz locale for an Alphabet (QWERTY) layout will not match any actual language.
|
|
||||||
String localeString = cursor.getString(dictLocaleIndex);
|
|
||||||
if (null == localeString) {
|
|
||||||
if (DebugFlags.DEBUG_ENABLED) {
|
|
||||||
Log.d(mTag, "loadPersonalDictionary() : Null locale for word [" +
|
|
||||||
rawDictWord + "], assuming all locales");
|
|
||||||
}
|
|
||||||
// For purposes of LocaleUtils, an empty locale matches everything.
|
|
||||||
localeString = "";
|
|
||||||
}
|
|
||||||
final Locale dictLocale = LocaleUtils.constructLocaleFromString(localeString);
|
|
||||||
// Lowercase the word before storing it.
|
|
||||||
final String dictWord = rawDictWord.toLowerCase(dictLocale);
|
|
||||||
if (DebugFlags.DEBUG_ENABLED) {
|
|
||||||
Log.d(mTag, "loadPersonalDictionary() : Adding word [" + dictWord
|
|
||||||
+ "] for locale " + dictLocale + "with value" + rawDictWord);
|
|
||||||
}
|
|
||||||
// Check if there is an existing entry for this word.
|
|
||||||
HashMap<Locale, String> dictLocales = dictWords.get(dictWord);
|
|
||||||
if (CollectionUtils.isNullOrEmpty(dictLocales)) {
|
|
||||||
// If there is no entry for this word, create one.
|
|
||||||
if (DebugFlags.DEBUG_ENABLED) {
|
|
||||||
Log.d(mTag, "loadPersonalDictionary() : Word [" + dictWord +
|
|
||||||
"] not seen for other locales, creating new entry");
|
|
||||||
}
|
|
||||||
dictLocales = new HashMap<>();
|
|
||||||
dictWords.put(dictWord, dictLocales);
|
|
||||||
}
|
|
||||||
// Append the locale to the list of locales this word is in.
|
|
||||||
dictLocales.put(dictLocale, rawDictWord);
|
|
||||||
|
|
||||||
// If there is no column for a shortcut, we're done.
|
|
||||||
final int shortcutIndex = cursor.getColumnIndex(UserDictionary.Words.SHORTCUT);
|
|
||||||
if (shortcutIndex < 0) {
|
|
||||||
if (DebugFlags.DEBUG_ENABLED) {
|
|
||||||
Log.d(mTag, "loadPersonalDictionary() : Entry without SHORTCUT, done");
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// If the shortcut is null, we're done.
|
|
||||||
final String shortcut = cursor.getString(shortcutIndex);
|
|
||||||
if (shortcut == null) {
|
|
||||||
if (DebugFlags.DEBUG_ENABLED) {
|
|
||||||
Log.d(mTag, "loadPersonalDictionary() : Null shortcut");
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Else, save the shortcut.
|
|
||||||
HashMap<String, String> localeShortcuts = shortcutsPerLocale.get(dictLocale);
|
|
||||||
if (localeShortcuts == null) {
|
|
||||||
localeShortcuts = new HashMap<>();
|
|
||||||
shortcutsPerLocale.put(dictLocale, localeShortcuts);
|
|
||||||
}
|
|
||||||
// Map to the raw input, which might be capitalized.
|
|
||||||
// This lets the user create a shortcut from "gm" to "General Motors".
|
|
||||||
localeShortcuts.put(shortcut, rawDictWord);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
List<DictionaryStats> stats = new ArrayList<>();
|
|
||||||
stats.add(new DictionaryStats(ANY_LOCALE, Dictionary.TYPE_USER, dictWords.size()));
|
|
||||||
int numShortcuts = 0;
|
|
||||||
for (HashMap<String, String> shortcuts : shortcutsPerLocale.values()) {
|
|
||||||
numShortcuts += shortcuts.size();
|
|
||||||
}
|
|
||||||
stats.add(new DictionaryStats(ANY_LOCALE, Dictionary.TYPE_USER_SHORTCUT, numShortcuts));
|
|
||||||
mDictionaryStats = stats;
|
|
||||||
|
|
||||||
// Atomically replace the copy of mDictWords and mShortcuts.
|
|
||||||
mDictWords = dictWords;
|
|
||||||
mShortcutsPerLocale = shortcutsPerLocale;
|
|
||||||
|
|
||||||
// Allow other calls to loadPersonalDictionary to execute now.
|
|
||||||
mIsLoading.set(false);
|
|
||||||
|
|
||||||
Log.i(mTag, "loadPersonalDictionary() : Loaded " + mDictWords.size()
|
|
||||||
+ " words and " + numShortcuts + " shortcuts");
|
|
||||||
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -35,6 +35,10 @@ public class NativeSuggestOptions {
|
||||||
setBooleanOption(IS_GESTURE, value);
|
setBooleanOption(IS_GESTURE, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setIsSpaceAwareGesture(final boolean value) {
|
||||||
|
setBooleanOption(SPACE_AWARE_GESTURE_ENABLED, value);
|
||||||
|
}
|
||||||
|
|
||||||
public void setUseFullEditDistance(final boolean value) {
|
public void setUseFullEditDistance(final boolean value) {
|
||||||
setBooleanOption(USE_FULL_EDIT_DISTANCE, value);
|
setBooleanOption(USE_FULL_EDIT_DISTANCE, value);
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,6 @@ import org.dslul.openboard.inputmethod.latin.common.InputPointers;
|
||||||
import org.dslul.openboard.inputmethod.latin.common.StringUtils;
|
import org.dslul.openboard.inputmethod.latin.common.StringUtils;
|
||||||
import org.dslul.openboard.inputmethod.latin.define.DebugFlags;
|
import org.dslul.openboard.inputmethod.latin.define.DebugFlags;
|
||||||
import org.dslul.openboard.inputmethod.latin.settings.SettingsValues;
|
import org.dslul.openboard.inputmethod.latin.settings.SettingsValues;
|
||||||
import org.dslul.openboard.inputmethod.latin.settings.SettingsValuesForSuggestion;
|
|
||||||
import org.dslul.openboard.inputmethod.latin.settings.SpacingAndPunctuations;
|
import org.dslul.openboard.inputmethod.latin.settings.SpacingAndPunctuations;
|
||||||
import org.dslul.openboard.inputmethod.latin.suggestions.SuggestionStripViewAccessor;
|
import org.dslul.openboard.inputmethod.latin.suggestions.SuggestionStripViewAccessor;
|
||||||
import org.dslul.openboard.inputmethod.latin.utils.AsyncResultHolder;
|
import org.dslul.openboard.inputmethod.latin.utils.AsyncResultHolder;
|
||||||
|
@ -2321,7 +2320,7 @@ public final class InputLogic {
|
||||||
// hence 2; if we aren't, we should just skip whitespace if any, so 1.
|
// hence 2; if we aren't, we should just skip whitespace if any, so 1.
|
||||||
mWordComposer.isComposingWord() ? 2 : 1),
|
mWordComposer.isComposingWord() ? 2 : 1),
|
||||||
keyboard,
|
keyboard,
|
||||||
new SettingsValuesForSuggestion(settingsValues.mBlockPotentiallyOffensive),
|
settingsValues.mSettingsValuesForSuggestion,
|
||||||
settingsValues.mAutoCorrectionEnabledPerUserSettings,
|
settingsValues.mAutoCorrectionEnabledPerUserSettings,
|
||||||
inputStyle, sequenceNumber, callback);
|
inputStyle, sequenceNumber, callback);
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,5 +51,7 @@ public final class GestureSettingsFragment extends SubScreenFragment {
|
||||||
Settings.readGestureInputEnabled(prefs, res));
|
Settings.readGestureInputEnabled(prefs, res));
|
||||||
setPreferenceVisible(Settings.PREF_GESTURE_FLOATING_PREVIEW_TEXT,
|
setPreferenceVisible(Settings.PREF_GESTURE_FLOATING_PREVIEW_TEXT,
|
||||||
Settings.readGestureInputEnabled(prefs, res));
|
Settings.readGestureInputEnabled(prefs, res));
|
||||||
|
setPreferenceVisible(Settings.PREF_GESTURE_SPACE_AWARE,
|
||||||
|
Settings.readGestureInputEnabled(prefs, res));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -110,6 +110,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
|
||||||
public static final String PREF_ENABLE_EMOJI_ALT_PHYSICAL_KEY = "pref_enable_emoji_alt_physical_key";
|
public static final String PREF_ENABLE_EMOJI_ALT_PHYSICAL_KEY = "pref_enable_emoji_alt_physical_key";
|
||||||
public static final String PREF_GESTURE_PREVIEW_TRAIL = "pref_gesture_preview_trail";
|
public static final String PREF_GESTURE_PREVIEW_TRAIL = "pref_gesture_preview_trail";
|
||||||
public static final String PREF_GESTURE_FLOATING_PREVIEW_TEXT = "pref_gesture_floating_preview_text";
|
public static final String PREF_GESTURE_FLOATING_PREVIEW_TEXT = "pref_gesture_floating_preview_text";
|
||||||
|
public static final String PREF_GESTURE_SPACE_AWARE = "pref_gesture_space_aware";
|
||||||
public static final String PREF_SHOW_SETUP_WIZARD_ICON = "pref_show_setup_wizard_icon";
|
public static final String PREF_SHOW_SETUP_WIZARD_ICON = "pref_show_setup_wizard_icon";
|
||||||
|
|
||||||
public static final String PREF_ONE_HANDED_MODE = "pref_one_handed_mode_enabled";
|
public static final String PREF_ONE_HANDED_MODE = "pref_one_handed_mode_enabled";
|
||||||
|
|
|
@ -123,6 +123,7 @@ public class SettingsValues {
|
||||||
public final float mPlausibilityThreshold;
|
public final float mPlausibilityThreshold;
|
||||||
public final boolean mAutoCorrectionEnabledPerUserSettings;
|
public final boolean mAutoCorrectionEnabledPerUserSettings;
|
||||||
private final boolean mSuggestionsEnabledPerUserSettings;
|
private final boolean mSuggestionsEnabledPerUserSettings;
|
||||||
|
public final SettingsValuesForSuggestion mSettingsValuesForSuggestion;
|
||||||
public final boolean mIncognitoModeEnabled;
|
public final boolean mIncognitoModeEnabled;
|
||||||
private final AsyncResultHolder<AppWorkaroundsUtils> mAppWorkarounds;
|
private final AsyncResultHolder<AppWorkaroundsUtils> mAppWorkarounds;
|
||||||
|
|
||||||
|
@ -260,6 +261,10 @@ public class SettingsValues {
|
||||||
mUseContactsDictionary = prefs.getBoolean(AndroidSpellCheckerService.PREF_USE_CONTACTS_KEY, false);
|
mUseContactsDictionary = prefs.getBoolean(AndroidSpellCheckerService.PREF_USE_CONTACTS_KEY, false);
|
||||||
mCustomNavBarColor = prefs.getBoolean(Settings.PREF_NAVBAR_COLOR, false);
|
mCustomNavBarColor = prefs.getBoolean(Settings.PREF_NAVBAR_COLOR, false);
|
||||||
mNarrowKeyGaps = prefs.getBoolean(Settings.PREF_NARROW_KEY_GAPS, true);
|
mNarrowKeyGaps = prefs.getBoolean(Settings.PREF_NARROW_KEY_GAPS, true);
|
||||||
|
mSettingsValuesForSuggestion = new SettingsValuesForSuggestion(
|
||||||
|
mBlockPotentiallyOffensive,
|
||||||
|
prefs.getBoolean(Settings.PREF_GESTURE_SPACE_AWARE, false)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isApplicationSpecifiedCompletionsOn() {
|
public boolean isApplicationSpecifiedCompletionsOn() {
|
||||||
|
|
|
@ -19,7 +19,13 @@ package org.dslul.openboard.inputmethod.latin.settings;
|
||||||
public class SettingsValuesForSuggestion {
|
public class SettingsValuesForSuggestion {
|
||||||
public final boolean mBlockPotentiallyOffensive;
|
public final boolean mBlockPotentiallyOffensive;
|
||||||
|
|
||||||
public SettingsValuesForSuggestion(final boolean blockPotentiallyOffensive) {
|
public SettingsValuesForSuggestion(
|
||||||
|
final boolean blockPotentiallyOffensive,
|
||||||
|
final boolean spaceAwareGesture
|
||||||
|
) {
|
||||||
mBlockPotentiallyOffensive = blockPotentiallyOffensive;
|
mBlockPotentiallyOffensive = blockPotentiallyOffensive;
|
||||||
|
mSpaceAwareGesture = spaceAwareGesture;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final boolean mSpaceAwareGesture;
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,8 +52,6 @@ import java.util.concurrent.Semaphore;
|
||||||
*/
|
*/
|
||||||
public final class AndroidSpellCheckerService extends SpellCheckerService
|
public final class AndroidSpellCheckerService extends SpellCheckerService
|
||||||
implements SharedPreferences.OnSharedPreferenceChangeListener {
|
implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
private static final String TAG = AndroidSpellCheckerService.class.getSimpleName();
|
|
||||||
private static final boolean DEBUG = false;
|
|
||||||
|
|
||||||
public static final String PREF_USE_CONTACTS_KEY = "pref_spellcheck_use_contacts";
|
public static final String PREF_USE_CONTACTS_KEY = "pref_spellcheck_use_contacts";
|
||||||
|
|
||||||
|
@ -78,7 +76,7 @@ public final class AndroidSpellCheckerService extends SpellCheckerService
|
||||||
private float mRecommendedThreshold;
|
private float mRecommendedThreshold;
|
||||||
// TODO: make a spell checker option to block offensive words or not
|
// TODO: make a spell checker option to block offensive words or not
|
||||||
private final SettingsValuesForSuggestion mSettingsValuesForSuggestion =
|
private final SettingsValuesForSuggestion mSettingsValuesForSuggestion =
|
||||||
new SettingsValuesForSuggestion(true /* blockPotentiallyOffensive */);
|
new SettingsValuesForSuggestion(true, false);
|
||||||
|
|
||||||
public static final String SINGLE_QUOTE = "\u0027";
|
public static final String SINGLE_QUOTE = "\u0027";
|
||||||
public static final String APOSTROPHE = "\u2019";
|
public static final String APOSTROPHE = "\u2019";
|
||||||
|
|
|
@ -35,4 +35,10 @@
|
||||||
android:title="@string/gesture_preview_trail"
|
android:title="@string/gesture_preview_trail"
|
||||||
android:defaultValue="true"
|
android:defaultValue="true"
|
||||||
android:persistent="true" />
|
android:persistent="true" />
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
android:key="pref_gesture_space_aware"
|
||||||
|
android:title="@string/gesture_space_aware"
|
||||||
|
android:summary="@string/gesture_space_aware_summary"
|
||||||
|
android:defaultValue="false"
|
||||||
|
android:persistent="true" />
|
||||||
</PreferenceScreen>
|
</PreferenceScreen>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue