diff options
Diffstat (limited to 'java')
12 files changed, 277 insertions, 111 deletions
diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml index 4973a99f5..28dabf682 100644 --- a/java/res/values/strings.xml +++ b/java/res/values/strings.xml @@ -129,6 +129,9 @@ <!-- Description for option to enable auto capitalization of sentences --> <string name="auto_cap_summary">Capitalize the first word of each sentence</string> + <!-- Option to edit personal dictionary. [CHAR_LIMIT=30]--> + <string name="edit_personal_dictionary">Personal dictionary</string> + <!-- Option to configure dictionaries --> <string name="configure_dictionaries_title">Add-on dictionaries</string> <!-- Name of the main dictionary, as opposed to auxiliary dictionaries (medical/entertainment/sports...) --> diff --git a/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java b/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java index a2789cc1a..fbc899192 100644 --- a/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java +++ b/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java @@ -34,6 +34,8 @@ import java.util.LinkedList; import java.util.List; import java.util.TreeMap; +import javax.annotation.Nullable; + /** * Various helper functions for the state database */ @@ -705,6 +707,7 @@ public class MetadataDbHelper extends SQLiteOpenHelper { * @param version the word list version. * @return the metadata about this word list. */ + @Nullable public static ContentValues getContentValuesByWordListId(final SQLiteDatabase db, final String id, final int version) { final Cursor cursor = db.query(METADATA_TABLE_NAME, diff --git a/java/src/com/android/inputmethod/dictionarypack/MetadataHandler.java b/java/src/com/android/inputmethod/dictionarypack/MetadataHandler.java index 329b9f62e..e5d632fbe 100644 --- a/java/src/com/android/inputmethod/dictionarypack/MetadataHandler.java +++ b/java/src/com/android/inputmethod/dictionarypack/MetadataHandler.java @@ -19,6 +19,7 @@ package com.android.inputmethod.dictionarypack; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; +import android.util.Log; import java.io.IOException; import java.io.InputStreamReader; @@ -30,10 +31,13 @@ import java.util.List; * Helper class to easy up manipulation of dictionary pack metadata. */ public class MetadataHandler { + + public static final String TAG = MetadataHandler.class.getSimpleName(); + // The canonical file name for metadata. This is not the name of a real file on the // device, but a symbolic name used in the database and in metadata handling. It is never // tested against, only used for human-readability as the file name for the metadata. - public final static String METADATA_FILENAME = "metadata.json"; + public static final String METADATA_FILENAME = "metadata.json"; /** * Reads the data from the cursor and store it in metadata objects. @@ -114,6 +118,14 @@ public class MetadataHandler { final String clientId, final String wordListId, final int version) { final ContentValues contentValues = MetadataDbHelper.getContentValuesByWordListId( MetadataDbHelper.getDb(context, clientId), wordListId, version); + if (contentValues == null) { + // TODO: Figure out why this would happen. + // Check if this happens when the metadata gets updated in the background. + Log.e(TAG, String.format( "Unable to find the current metadata for wordlist " + + "(clientId=%s, wordListId=%s, version=%d) on the database", + clientId, wordListId, version)); + return null; + } return WordListMetadata.createFromContentValues(contentValues); } diff --git a/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java b/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java index 30ff0b8ee..e720f3cd0 100644 --- a/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java +++ b/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java @@ -1143,6 +1143,9 @@ public final class UpdateHandler { } final WordListMetadata wordListMetaData = MetadataHandler.getCurrentMetadataForWordList( context, clientId, wordlistId, version); + if (wordListMetaData == null) { + return; + } final ActionBatch actions = new ActionBatch(); actions.add(new ActionBatch.StartDownloadAction( diff --git a/java/src/com/android/inputmethod/dictionarypack/WordListMetadata.java b/java/src/com/android/inputmethod/dictionarypack/WordListMetadata.java index 59f75e4ed..99cffb816 100644 --- a/java/src/com/android/inputmethod/dictionarypack/WordListMetadata.java +++ b/java/src/com/android/inputmethod/dictionarypack/WordListMetadata.java @@ -18,6 +18,8 @@ package com.android.inputmethod.dictionarypack; import android.content.ContentValues; +import javax.annotation.Nonnull; + /** * The metadata for a single word list. * @@ -77,7 +79,7 @@ public class WordListMetadata { * * If this lacks any required field, IllegalArgumentException is thrown. */ - public static WordListMetadata createFromContentValues(final ContentValues values) { + public static WordListMetadata createFromContentValues(@Nonnull final ContentValues values) { final String id = values.getAsString(MetadataDbHelper.WORDLISTID_COLUMN); final Integer type = values.getAsInteger(MetadataDbHelper.TYPE_COLUMN); final String description = values.getAsString(MetadataDbHelper.DESCRIPTION_COLUMN); diff --git a/java/src/com/android/inputmethod/latin/Dictionary.java b/java/src/com/android/inputmethod/latin/Dictionary.java index 16dcb3208..e00219c98 100644 --- a/java/src/com/android/inputmethod/latin/Dictionary.java +++ b/java/src/com/android/inputmethod/latin/Dictionary.java @@ -39,6 +39,10 @@ public abstract class Dictionary { public static final String TYPE_USER_TYPED = "user_typed"; public static final PhonyDictionary DICTIONARY_USER_TYPED = new PhonyDictionary(TYPE_USER_TYPED); + public static final String TYPE_USER_SHORTCUT = "user_shortcut"; + public static final PhonyDictionary DICTIONARY_USER_SHORTCUT = + new PhonyDictionary(TYPE_USER_SHORTCUT); + public static final String TYPE_APPLICATION_DEFINED = "application_defined"; public static final PhonyDictionary DICTIONARY_APPLICATION_DEFINED = new PhonyDictionary(TYPE_APPLICATION_DEFINED); diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java index 37899d21e..907095746 100644 --- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java @@ -650,7 +650,8 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { reloadDictionaryIfRequired(); final String dictName = mDictName; final File dictFile = mDictFile; - final AsyncResultHolder<DictionaryStats> result = new AsyncResultHolder<>(); + final AsyncResultHolder<DictionaryStats> result = + new AsyncResultHolder<>("DictionaryStats"); asyncExecuteTaskWithLock(mLock.readLock(), new Runnable() { @Override public void run() { @@ -724,7 +725,8 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { */ public WordProperty[] getWordPropertiesForSyncing() { reloadDictionaryIfRequired(); - final AsyncResultHolder<WordProperty[]> result = new AsyncResultHolder<>(); + final AsyncResultHolder<WordProperty[]> result = + new AsyncResultHolder<>("WordPropertiesForSync"); asyncExecuteTaskWithLock(mLock.readLock(), new Runnable() { @Override public void run() { diff --git a/java/src/com/android/inputmethod/latin/spellcheck/UserDictionaryLookup.java b/java/src/com/android/inputmethod/latin/UserDictionaryLookup.java index f2491f478..2569723b0 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/UserDictionaryLookup.java +++ b/java/src/com/android/inputmethod/latin/UserDictionaryLookup.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.inputmethod.latin.spellcheck; +package com.android.inputmethod.latin; import android.content.ContentResolver; import android.content.Context; @@ -22,9 +22,11 @@ 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 com.android.inputmethod.annotations.UsedForTesting; +import com.android.inputmethod.latin.common.CollectionUtils; import com.android.inputmethod.latin.common.LocaleUtils; import com.android.inputmethod.latin.utils.ExecutorUtils; @@ -36,6 +38,9 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + /** * UserDictionaryLookup provides the ability to lookup into the system-wide "Personal dictionary". * @@ -47,7 +52,6 @@ import java.util.concurrent.TimeUnit; * onCreate and close() it in onDestroy. */ public class UserDictionaryLookup implements Closeable { - private static final String TAG = UserDictionaryLookup.class.getSimpleName(); /** * This guards the execution of any Log.d() logging, so that if false, they are not even @@ -79,7 +83,12 @@ public class UserDictionaryLookup implements Closeable { @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; /** * Runnable that calls loadUserDictionary(). @@ -88,12 +97,11 @@ public class UserDictionaryLookup implements Closeable { @Override public void run() { if (DEBUG) { - Log.d(TAG, "Executing (re)load"); + Log.d(mTag, "Executing (re)load"); } loadUserDictionary(); } } - private final UserDictionaryLoader mLoader = new UserDictionaryLoader(); /** * Content observer for UserDictionary changes. It has the following properties: @@ -122,7 +130,7 @@ public class UserDictionaryLookup implements Closeable { @Override public void onChange(boolean selfChange, Uri uri) { if (DEBUG) { - Log.d(TAG, "Received content observer onChange notification for URI: " + uri); + Log.d(mTag, "Received content observer onChange notification for URI: " + uri); } // Cancel (but don't interrupt) any pending reloads (except the initial load). if (mReloadFuture != null && !mReloadFuture.isCancelled() && @@ -131,20 +139,20 @@ public class UserDictionaryLookup implements Closeable { boolean isCancelled = mReloadFuture.cancel(false); if (DEBUG) { if (isCancelled) { - Log.d(TAG, "Successfully canceled previous reload request"); + Log.d(mTag, "Successfully canceled previous reload request"); } else { - Log.d(TAG, "Unable to cancel previous reload request"); + Log.d(mTag, "Unable to cancel previous reload request"); } } } if (DEBUG) { - Log.d(TAG, "Scheduling reload in " + RELOAD_DELAY_MS + " ms"); + Log.d(mTag, "Scheduling reload in " + RELOAD_DELAY_MS + " ms"); } // Schedule a new reload after RELOAD_DELAY_MS. - mReloadFuture = ExecutorUtils.getBackgroundExecutor(ExecutorUtils.SPELLING) - .schedule(mLoader, RELOAD_DELAY_MS, TimeUnit.MILLISECONDS); + mReloadFuture = ExecutorUtils.getBackgroundExecutor(mServiceName) + .schedule(new UserDictionaryLoader(), RELOAD_DELAY_MS, TimeUnit.MILLISECONDS); } } private final ContentObserver mObserver = new UserDictionaryContentObserver(); @@ -167,6 +175,12 @@ public class UserDictionaryLookup implements Closeable { private volatile HashMap<String, ArrayList<Locale>> 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. */ @@ -175,18 +189,24 @@ public class UserDictionaryLookup implements Closeable { /** * @param context the context from which to obtain content resolver */ - public UserDictionaryLookup(Context context) { - if (DEBUG) { - Log.d(TAG, "UserDictionaryLookup constructor with context: " + context); - } + public UserDictionaryLookup(@Nonnull final Context context, @Nonnull final String serviceName) { + mTag = serviceName + ".UserDictionaryLookup"; + + Log.i(mTag, "create()"); + + mServiceName = serviceName; // Obtain a content resolver. mResolver = context.getContentResolver(); + } + + 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. - ExecutorUtils.getBackgroundExecutor(ExecutorUtils.SPELLING).execute(mLoader); + loadUserDictionary(); // Register the observer to be notified on changes to the UserDictionary and all individual // items. @@ -210,7 +230,7 @@ public class UserDictionaryLookup implements Closeable { public void finalize() throws Throwable { try { if (DEBUG) { - Log.d(TAG, "Finalize called, calling close()"); + Log.d(mTag, "Finalize called, calling close()"); } close(); } finally { @@ -227,7 +247,7 @@ public class UserDictionaryLookup implements Closeable { @Override public void close() { if (DEBUG) { - Log.d(TAG, "Close called (no pun intended), cleaning up executor and observer"); + Log.d(mTag, "Close called (no pun intended), cleaning up executor and observer"); } if (mIsClosed.compareAndSet(false, true)) { // Unregister the content observer. @@ -240,9 +260,8 @@ public class UserDictionaryLookup implements Closeable { * * @return true if the initial load is successful */ - @UsedForTesting - boolean isLoaded() { - return mDictWords != null; + public boolean isLoaded() { + return mDictWords != null && mShortcutsPerLocale != null; } /** @@ -255,14 +274,13 @@ public class UserDictionaryLookup implements Closeable { * @param locale the locale in which to match the word * @return true iff the word has been matched for this locale in the UserDictionary. */ - public boolean isValidWord( - final String word, final Locale locale) { + public boolean isValidWord(@Nonnull final String word, @Nonnull final Locale locale) { if (!isLoaded()) { // This is a corner case in the event the initial load of UserDictionary has not // been loaded. In that case, we assume the word is not a valid word in // UserDictionary. if (DEBUG) { - Log.d(TAG, "isValidWord invoked, but initial load not complete"); + Log.d(mTag, "isValidWord invoked, but initial load not complete"); } return false; } @@ -271,7 +289,7 @@ public class UserDictionaryLookup implements Closeable { final HashMap<String, ArrayList<Locale>> dictWords = mDictWords; if (DEBUG) { - Log.d(TAG, "isValidWord invoked for word [" + word + + Log.d(mTag, "isValidWord invoked for word [" + word + "] in locale " + locale); } // Lowercase the word using the given locale. Note, that dictionary @@ -282,13 +300,13 @@ public class UserDictionaryLookup implements Closeable { final ArrayList<Locale> dictLocales = dictWords.get(lowercased); if (null == dictLocales) { if (DEBUG) { - Log.d(TAG, "isValidWord=false, since there is no entry for " + + Log.d(mTag, "isValidWord=false, since there is no entry for " + "lowercased word [" + lowercased + "]"); } return false; } else { if (DEBUG) { - Log.d(TAG, "isValidWord found an entry for lowercased word [" + lowercased + + Log.d(mTag, "isValidWord found an entry for lowercased word [" + lowercased + "]; examining locales"); } // Iterate over the locales this word is in. @@ -296,28 +314,90 @@ public class UserDictionaryLookup implements Closeable { final int matchLevel = LocaleUtils.getMatchLevel(dictLocale.toString(), locale.toString()); if (DEBUG) { - Log.d(TAG, "matchLevel for dictLocale=" + dictLocale + ", locale=" + + Log.d(mTag, "matchLevel for dictLocale=" + dictLocale + ", locale=" + locale + " is " + matchLevel); } if (LocaleUtils.isMatch(matchLevel)) { if (DEBUG) { - Log.d(TAG, "isValidWord=true, since matchLevel " + matchLevel + + Log.d(mTag, "isValidWord=true, since matchLevel " + matchLevel + " is a match"); } return true; } if (DEBUG) { - Log.d(TAG, "matchLevel " + matchLevel + " is not a match"); + Log.d(mTag, "matchLevel " + matchLevel + " is not a match"); } } if (DEBUG) { - Log.d(TAG, "isValidWord=false, since none of the locales matched"); + 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 UserDictionary. + */ + @Nullable public String expandShortcut( + @Nonnull final String shortcut, @Nonnull final Locale inputLocale) { + if (DEBUG) { + 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)) { + 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)) { + 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)) { + return expansionForLanguage; + } + + // If all else fails, loof for a global shortcut. + return expandShortcut(shortcutsPerLocale, shortcut, ANY_LOCALE); + } + + @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; + } + final String word = localeShortcuts.get(shortcut); + if (DEBUG && word != null) { + Log.d(mTag, "expandShortcut() : Shortcut [" + shortcut + "] for [" + locale + + "] expands to [" + word + "]"); + } + return word; + } + + /** * Loads the UserDictionary in the current thread. * * Only one reload can happen at a time. If already running, will exit quickly. @@ -325,45 +405,36 @@ public class UserDictionaryLookup implements Closeable { private void loadUserDictionary() { // Bail out if already in the process of loading. if (!mIsLoading.compareAndSet(false, true)) { - if (DEBUG) { - Log.d(TAG, "Already in the process of loading UserDictionary, skipping"); - } + Log.i(mTag, "loadUserDictionary() : Already Loading (exit)"); return; } - if (DEBUG) { - Log.d(TAG, "Loading UserDictionary"); - } + Log.i(mTag, "loadUserDictionary() : Start Loading"); HashMap<String, ArrayList<Locale>> dictWords = new HashMap<>(); + HashMap<Locale, HashMap<String, String>> shortcutsPerLocale = new HashMap<>(); // Load the UserDictionary. Request that items be returned in the default sort order // for UserDictionary, which is by frequency. Cursor cursor = mResolver.query(UserDictionary.Words.CONTENT_URI, null, null, null, UserDictionary.Words.DEFAULT_SORT_ORDER); if (null == cursor || cursor.getCount() < 1) { - if (DEBUG) { - Log.d(TAG, "No entries found in UserDictionary"); - } + Log.i(mTag, "loadUserDictionary() : Empty"); } else { // Iterate over the entries in the UserDictionary. 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); + final int dictLocaleIndex = cursor.getColumnIndex(UserDictionary.Words.LOCALE); if (dictLocaleIndex < 0) { if (DEBUG) { - Log.d(TAG, "Encountered UserDictionary entry " + - "without LOCALE, skipping"); + Log.d(mTag, "Encountered UserDictionary entry without LOCALE, skipping"); } continue; } // If there is no column for word, skip this entry. - final int dictWordIndex = cursor.getColumnIndex( - UserDictionary.Words.WORD); + final int dictWordIndex = cursor.getColumnIndex(UserDictionary.Words.WORD); if (dictWordIndex < 0) { if (DEBUG) { - Log.d(TAG, "Encountered UserDictionary entry without " + - "WORD, skipping"); + Log.d(mTag, "Encountered UserDictionary entry without WORD, skipping"); } continue; } @@ -371,7 +442,7 @@ public class UserDictionaryLookup implements Closeable { final String rawDictWord = cursor.getString(dictWordIndex); if (null == rawDictWord) { if (DEBUG) { - Log.d(TAG, "Encountered null word"); + Log.d(mTag, "Encountered null word"); } continue; } @@ -380,19 +451,17 @@ public class UserDictionaryLookup implements Closeable { String localeString = cursor.getString(dictLocaleIndex); if (null == localeString) { if (DEBUG) { - Log.d(TAG, "Encountered null locale for word [" + + Log.d(mTag, "Encountered null locale for word [" + rawDictWord + "], assuming all locales"); } - // For purposes of LocaleUtils, an empty locale matches - // everything. + // For purposes of LocaleUtils, an empty locale matches everything. localeString = ""; } - final Locale dictLocale = LocaleUtils.constructLocaleFromString( - localeString); + final Locale dictLocale = LocaleUtils.constructLocaleFromString(localeString); // Lowercase the word before storing it. final String dictWord = rawDictWord.toLowerCase(dictLocale); if (DEBUG) { - Log.d(TAG, "Incorporating UserDictionary word [" + dictWord + + Log.d(mTag, "Incorporating UserDictionary word [" + dictWord + "] for locale " + dictLocale); } // Check if there is an existing entry for this word. @@ -400,7 +469,7 @@ public class UserDictionaryLookup implements Closeable { if (null == dictLocales) { // If there is no entry for this word, create one. if (DEBUG) { - Log.d(TAG, "Word [" + dictWord + + Log.d(mTag, "Word [" + dictWord + "] not seen for other locales, creating new entry"); } dictLocales = new ArrayList<>(); @@ -408,13 +477,42 @@ public class UserDictionaryLookup implements Closeable { } // Append the locale to the list of locales this word is in. dictLocales.add(dictLocale); + + // If there is no column for a shortcut, we're done. + final int shortcutIndex = cursor.getColumnIndex(UserDictionary.Words.SHORTCUT); + if (shortcutIndex < 0) { + if (DEBUG) { + Log.d(mTag, "Encountered UserDictionary entry without SHORTCUT, done"); + } + continue; + } + // If the shortcut is null, we're done. + final String shortcut = cursor.getString(shortcutIndex); + if (shortcut == null) { + if (DEBUG) { + Log.d(mTag, "Encountered 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); } } - // Atomically replace the copy of mDictWords. + // Atomically replace the copy of mDictWords and mShortcuts. mDictWords = dictWords; + mShortcutsPerLocale = shortcutsPerLocale; // Allow other calls to loadUserDictionary to execute now. mIsLoading.set(false); + + Log.i(mTag, "loadUserDictionary() : Loaded " + mDictWords.size() + " words"); } } diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java index cf4064b72..9ceb37145 100644 --- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java +++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java @@ -104,6 +104,10 @@ public final class InputLogic { private boolean mIsAutoCorrectionIndicatorOn; private long mDoubleSpacePeriodCountdownStart; + // The word being corrected while the cursor is in the middle of the word. + // Note: This does not have a composing span, so it must be handled separately. + private String mWordBeingCorrectedByCursor = null; + /** * Create a new instance of the input logic. * @param latinIME the instance of the parent LatinIME. We should remove this when we can. @@ -133,6 +137,7 @@ public final class InputLogic { */ public void startInput(final String combiningSpec, final SettingsValues settingsValues) { mEnteredText = null; + mWordBeingCorrectedByCursor = null; if (!mWordComposer.getTypedWord().isEmpty()) { // For messaging apps that offer send button, the IME does not get the opportunity // to capture the last word. This block should capture those uncommitted words. @@ -247,6 +252,7 @@ public final class InputLogic { // Space state must be updated before calling updateShiftState mSpaceState = SpaceState.NONE; mEnteredText = text; + mWordBeingCorrectedByCursor = null; inputTransaction.setDidAffectContents(); inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW); return inputTransaction; @@ -386,6 +392,15 @@ public final class InputLogic { // it here, which means we'll keep outdated suggestions for a split second but the // visual result is better. resetEntireInputState(newSelStart, newSelEnd, false /* clearSuggestionStrip */); + // If the user is in the middle of correcting a word, we should learn it before moving + // the cursor away. + if (!TextUtils.isEmpty(mWordBeingCorrectedByCursor)) { + final int timeStampInSeconds = (int)TimeUnit.MILLISECONDS.toSeconds( + System.currentTimeMillis()); + mDictionaryFacilitator.addToUserHistory(mWordBeingCorrectedByCursor, false, + NgramContext.EMPTY_PREV_WORDS_INFO, timeStampInSeconds, + settingsValues.mBlockPotentiallyOffensive); + } } else { // resetEntireInputState calls resetCachesUponCursorMove, but forcing the // composition to end. But in all cases where we don't reset the entire input @@ -401,6 +416,7 @@ public final class InputLogic { mLatinIME.mHandler.postResumeSuggestions(true /* shouldDelay */); // Stop the last recapitalization, if started. mRecapitalizeStatus.stop(); + mWordBeingCorrectedByCursor = null; return true; } @@ -420,6 +436,7 @@ public final class InputLogic { public InputTransaction onCodeInput(final SettingsValues settingsValues, @Nonnull final Event event, final int keyboardShiftMode, final int currentKeyboardScriptId, final LatinIME.UIHandler handler) { + mWordBeingCorrectedByCursor = null; final Event processedEvent = mWordComposer.processEvent(event); final InputTransaction inputTransaction = new InputTransaction(settingsValues, processedEvent, SystemClock.uptimeMillis(), mSpaceState, @@ -453,6 +470,14 @@ public final class InputLogic { } currentEvent = currentEvent.mNextEvent; } + // Try to record the word being corrected when the user enters a word character or + // the backspace key. + if (!mWordComposer.isComposingWord() + && (settingsValues.isWordCodePoint(processedEvent.mCodePoint) || + processedEvent.mKeyCode == Constants.CODE_DELETE)) { + mWordBeingCorrectedByCursor = getWordAtCursor( + settingsValues, currentKeyboardScriptId); + } if (!inputTransaction.didAutoCorrect() && processedEvent.mKeyCode != Constants.CODE_SHIFT && processedEvent.mKeyCode != Constants.CODE_CAPSLOCK && processedEvent.mKeyCode != Constants.CODE_SWITCH_ALPHA_SYMBOL) @@ -466,6 +491,7 @@ public final class InputLogic { public void onStartBatchInput(final SettingsValues settingsValues, final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler) { + mWordBeingCorrectedByCursor = null; mInputLogicHandler.onStartBatchInput(); handler.showGesturePreviewAndSuggestionStrip( SuggestedWords.getEmptyInstance(), false /* dismissGestureFloatingPreviewText */); @@ -1151,27 +1177,30 @@ public final class InputLogic { } } - boolean unlearnWordBeingDeleted( - final SettingsValues settingsValues,final int currentKeyboardScriptId) { - // If we just started backspacing to delete a previous word (but have not - // entered the composing state yet), unlearn the word. - // TODO: Consider tracking whether or not this word was typed by the user. + String getWordAtCursor(final SettingsValues settingsValues, final int currentKeyboardScriptId) { if (!mConnection.hasSelection() && settingsValues.isSuggestionsEnabledPerUserSettings() - && settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces - && !mConnection.isCursorFollowedByWordCharacter( - settingsValues.mSpacingAndPunctuations)) { + && settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces) { final TextRange range = mConnection.getWordRangeAtCursor( settingsValues.mSpacingAndPunctuations, currentKeyboardScriptId); - if (range == null) { - // Happens if we don't have an input connection at all. - return false; + if (range != null) { + return range.mWord.toString(); } - final String wordBeingDeleted = range.mWord.toString(); - if (!wordBeingDeleted.isEmpty()) { - unlearnWord(wordBeingDeleted, settingsValues, - Constants.EVENT_BACKSPACE); + } + return ""; + } + + boolean unlearnWordBeingDeleted( + final SettingsValues settingsValues, final int currentKeyboardScriptId) { + // If we just started backspacing to delete a previous word (but have not + // entered the composing state yet), unlearn the word. + // TODO: Consider tracking whether or not this word was typed by the user. + if (!mConnection.isCursorFollowedByWordCharacter(settingsValues.mSpacingAndPunctuations)) { + final String wordBeingDeleted = getWordAtCursor( + settingsValues, currentKeyboardScriptId); + if (!TextUtils.isEmpty(wordBeingDeleted)) { + unlearnWord(wordBeingDeleted, settingsValues, Constants.EVENT_BACKSPACE); return true; } } @@ -1407,7 +1436,7 @@ public final class InputLogic { return; } - final AsyncResultHolder<SuggestedWords> holder = new AsyncResultHolder<>(); + final AsyncResultHolder<SuggestedWords> holder = new AsyncResultHolder<>("Suggest"); mInputLogicHandler.getSuggestedWords(inputStyle, SuggestedWords.NOT_A_SEQUENCE_NUMBER, new OnGetSuggestedWordsCallback() { @Override @@ -2075,24 +2104,60 @@ public final class InputLogic { */ private void commitChosenWord(final SettingsValues settingsValues, final String chosenWord, final int commitType, final String separatorString) { + long startTimeMillis = 0; + if (DebugFlags.DEBUG_ENABLED) { + startTimeMillis = System.currentTimeMillis(); + Log.d(TAG, "commitChosenWord() : [" + chosenWord + "]"); + } final SuggestedWords suggestedWords = mSuggestedWords; final CharSequence chosenWordWithSuggestions = SuggestionSpanUtils.getTextWithSuggestionSpan(mLatinIME, chosenWord, suggestedWords); + if (DebugFlags.DEBUG_ENABLED) { + long runTimeMillis = System.currentTimeMillis() - startTimeMillis; + Log.d(TAG, "commitChosenWord() : " + runTimeMillis + " ms to run " + + "SuggestionSpanUtils.getTextWithSuggestionSpan()"); + startTimeMillis = System.currentTimeMillis(); + } // When we are composing word, get n-gram context from the 2nd previous word because the // 1st previous word is the word to be committed. Otherwise get n-gram context from the 1st // previous word. final NgramContext ngramContext = mConnection.getNgramContextFromNthPreviousWord( settingsValues.mSpacingAndPunctuations, mWordComposer.isComposingWord() ? 2 : 1); + if (DebugFlags.DEBUG_ENABLED) { + long runTimeMillis = System.currentTimeMillis() - startTimeMillis; + Log.d(TAG, "commitChosenWord() : " + runTimeMillis + " ms to run " + + "Connection.getNgramContextFromNthPreviousWord()"); + Log.d(TAG, "commitChosenWord() : NgramContext = " + ngramContext); + startTimeMillis = System.currentTimeMillis(); + } mConnection.commitText(chosenWordWithSuggestions, 1); + if (DebugFlags.DEBUG_ENABLED) { + long runTimeMillis = System.currentTimeMillis() - startTimeMillis; + Log.d(TAG, "commitChosenWord() : " + runTimeMillis + " ms to run " + + "Connection.commitText"); + startTimeMillis = System.currentTimeMillis(); + } // Add the word to the user history dictionary performAdditionToUserHistoryDictionary(settingsValues, chosenWord, ngramContext); + if (DebugFlags.DEBUG_ENABLED) { + long runTimeMillis = System.currentTimeMillis() - startTimeMillis; + Log.d(TAG, "commitChosenWord() : " + runTimeMillis + " ms to run " + + "performAdditionToUserHistoryDictionary()"); + startTimeMillis = System.currentTimeMillis(); + } // TODO: figure out here if this is an auto-correct or if the best word is actually // what user typed. Note: currently this is done much later in // LastComposedWord#didCommitTypedWord by string equality of the remembered // strings. mLastComposedWord = mWordComposer.commitWord(commitType, chosenWordWithSuggestions, separatorString, ngramContext); + if (DebugFlags.DEBUG_ENABLED) { + long runTimeMillis = System.currentTimeMillis() - startTimeMillis; + Log.d(TAG, "commitChosenWord() : " + runTimeMillis + " ms to run " + + "WordComposer.commitWord()"); + startTimeMillis = System.currentTimeMillis(); + } } /** diff --git a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java index d112e7200..94573a6d5 100644 --- a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java +++ b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java @@ -209,7 +209,7 @@ public class SettingsValues { prefs, DebugSettings.PREF_KEY_PREVIEW_DISMISS_END_Y_SCALE, defaultKeyPreviewDismissEndScale); mDisplayOrientation = res.getConfiguration().orientation; - mAppWorkarounds = new AsyncResultHolder<>(); + mAppWorkarounds = new AsyncResultHolder<>("AppWorkarounds"); final PackageInfo packageInfo = TargetPackageInfoGetterTask.getCachedPackageInfo( mInputAttributes.mTargetApplicationPackageName); if (null != packageInfo) { diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java index 766b385a9..4625e8e8b 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java @@ -24,7 +24,6 @@ import android.text.InputType; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodSubtype; import android.view.textservice.SuggestionsInfo; -import android.util.Log; import com.android.inputmethod.keyboard.Keyboard; import com.android.inputmethod.keyboard.KeyboardId; @@ -83,7 +82,6 @@ public final class AndroidSpellCheckerService extends SpellCheckerService public static final String SINGLE_QUOTE = "\u0027"; public static final String APOSTROPHE = "\u2019"; - private UserDictionaryLookup mUserDictionaryLookup; public AndroidSpellCheckerService() { super(); @@ -95,30 +93,11 @@ public final class AndroidSpellCheckerService extends SpellCheckerService @Override public void onCreate() { super.onCreate(); - mRecommendedThreshold = - Float.parseFloat(getString(R.string.spellchecker_recommended_threshold_value)); + mRecommendedThreshold = Float.parseFloat( + getString(R.string.spellchecker_recommended_threshold_value)); final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); prefs.registerOnSharedPreferenceChangeListener(this); onSharedPreferenceChanged(prefs, PREF_USE_CONTACTS_KEY); - // Create a UserDictionaryLookup. It needs to be close()d and set to null in onDestroy. - if (mUserDictionaryLookup == null) { - if (DEBUG) { - Log.d(TAG, "Creating mUserDictionaryLookup in onCreate"); - } - mUserDictionaryLookup = new UserDictionaryLookup(this); - } else if (DEBUG) { - Log.d(TAG, "mUserDictionaryLookup already created before onCreate"); - } - } - - @Override - public void onDestroy() { - if (DEBUG) { - Log.d(TAG, "Closing and dereferencing mUserDictionaryLookup in onDestroy"); - } - mUserDictionaryLookup.close(); - mUserDictionaryLookup = null; - super.onDestroy(); } public float getRecommendedThreshold() { @@ -181,16 +160,6 @@ public final class AndroidSpellCheckerService extends SpellCheckerService public boolean isValidWord(final Locale locale, final String word) { mSemaphore.acquireUninterruptibly(); try { - if (mUserDictionaryLookup.isValidWord(word, locale)) { - if (DEBUG) { - Log.d(TAG, "mUserDictionaryLookup.isValidWord(" + word + ")=true"); - } - return true; - } else { - if (DEBUG) { - Log.d(TAG, "mUserDictionaryLookup.isValidWord(" + word + ")=false"); - } - } DictionaryFacilitator dictionaryFacilitatorForLocale = mDictionaryFacilitatorCache.get(locale); return dictionaryFacilitatorForLocale.isValidSpellingWord(word); diff --git a/java/src/com/android/inputmethod/latin/utils/AsyncResultHolder.java b/java/src/com/android/inputmethod/latin/utils/AsyncResultHolder.java index 952ac2a62..1525f2d56 100644 --- a/java/src/com/android/inputmethod/latin/utils/AsyncResultHolder.java +++ b/java/src/com/android/inputmethod/latin/utils/AsyncResultHolder.java @@ -16,6 +16,8 @@ package com.android.inputmethod.latin.utils; +import android.util.Log; + import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -29,9 +31,11 @@ public class AsyncResultHolder<E> { private final Object mLock = new Object(); private E mResult; + private final String mTag; private final CountDownLatch mLatch; - public AsyncResultHolder() { + public AsyncResultHolder(final String tag) { + mTag = tag; mLatch = new CountDownLatch(1); } @@ -61,6 +65,7 @@ public class AsyncResultHolder<E> { try { return mLatch.await(timeOut, TimeUnit.MILLISECONDS) ? mResult : defaultValue; } catch (InterruptedException e) { + Log.w(mTag, "get() : Interrupted after " + timeOut + " ms"); return defaultValue; } } |