diff options
Diffstat (limited to 'java/src/com/android/inputmethod/latin')
14 files changed, 410 insertions, 133 deletions
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java index 660e50410..1fe0a4cce 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java @@ -29,6 +29,7 @@ import android.util.Log; import com.android.inputmethod.dictionarypack.DictionaryPackConstants; import com.android.inputmethod.dictionarypack.MD5Calculator; +import com.android.inputmethod.dictionarypack.UpdateHandler; import com.android.inputmethod.latin.common.FileUtils; import com.android.inputmethod.latin.define.DecoderSpecificConstants; import com.android.inputmethod.latin.utils.DictionaryInfoUtils; @@ -221,11 +222,11 @@ public final class BinaryDictionaryFileDumper { } /** - * Caches a word list the id of which is passed as an argument. This will write the file + * Stages a word list the id of which is passed as an argument. This will write the file * to the cache file name designated by its id and locale, overwriting it if already present * and creating it (and its containing directory) if necessary. */ - private static void cacheWordList(final String wordlistId, final String locale, + private static void installWordListToStaging(final String wordlistId, final String locale, final String rawChecksum, final ContentProviderClient providerClient, final Context context) { final int COMPRESSED_CRYPTED_COMPRESSED = 0; @@ -247,7 +248,7 @@ public final class BinaryDictionaryFileDumper { return; } final String finalFileName = - DictionaryInfoUtils.getCacheFileName(wordlistId, locale, context); + DictionaryInfoUtils.getStagingFileName(wordlistId, locale, context); String tempFileName; try { tempFileName = BinaryDictionaryGetter.getTempFileName(wordlistId, context); @@ -321,6 +322,7 @@ public final class BinaryDictionaryFileDumper { } } + // move the output file to the final staging file. final File finalFile = new File(finalFileName); if (!FileUtils.renameTo(outputFile, finalFile)) { Log.e(TAG, String.format("Failed to rename from %s to %s.", @@ -332,13 +334,12 @@ public final class BinaryDictionaryFileDumper { if (0 >= providerClient.delete(wordListUriBuilder.build(), null, null)) { Log.e(TAG, "Could not have the dictionary pack delete a word list"); } - BinaryDictionaryGetter.removeFilesWithIdExcept(context, wordlistId, finalFile); - Log.e(TAG, "Successfully copied file for wordlist ID " + wordlistId); + Log.d(TAG, "Successfully copied file for wordlist ID " + wordlistId); // Success! Close files (through the finally{} clause) and return. return; } catch (Exception e) { if (DEBUG) { - Log.i(TAG, "Can't open word list in mode " + mode, e); + Log.e(TAG, "Can't open word list in mode " + mode, e); } if (null != outputFile) { // This may or may not fail. The file may not have been created if the @@ -405,7 +406,7 @@ public final class BinaryDictionaryFileDumper { } /** - * Queries a content provider for word list data for some locale and cache the returned files + * Queries a content provider for word list data for some locale and stage the returned files * * This will query a content provider for word list data for a given locale, and copy the * files locally so that they can be mmap'ed. This may overwrite previously cached word lists @@ -413,7 +414,7 @@ public final class BinaryDictionaryFileDumper { * @throw FileNotFoundException if the provider returns non-existent data. * @throw IOException if the provider-returned data could not be read. */ - public static void cacheWordListsFromContentProvider(final Locale locale, + public static void installDictToStagingFromContentProvider(final Locale locale, final Context context, final boolean hasDefaultWordList) { final ContentProviderClient providerClient; try { @@ -431,7 +432,8 @@ public final class BinaryDictionaryFileDumper { final List<WordListInfo> idList = getWordListWordListInfos(locale, context, hasDefaultWordList); for (WordListInfo id : idList) { - cacheWordList(id.mId, id.mLocale, id.mRawChecksum, providerClient, context); + installWordListToStaging(id.mId, id.mLocale, id.mRawChecksum, providerClient, + context); } } finally { providerClient.release(); @@ -439,6 +441,18 @@ public final class BinaryDictionaryFileDumper { } /** + * Downloads the dictionary if it was never requested/used. + * + * @param locale locale to download + * @param context the context for resources and providers. + * @param hasDefaultWordList whether the default wordlist exists in the resources. + */ + public static void downloadDictIfNeverRequested(final Locale locale, + final Context context, final boolean hasDefaultWordList) { + getWordListWordListInfos(locale, context, hasDefaultWordList); + } + + /** * Copies the data in an input stream to a target file if the magic number matches. * * If the magic number does not match the expected value, this method throws an @@ -477,6 +491,8 @@ public final class BinaryDictionaryFileDumper { private static void reinitializeClientRecordInDictionaryContentProvider(final Context context, final ContentProviderClient client, final String clientId) throws RemoteException { final String metadataFileUri = MetadataFileUriGetter.getMetadataUri(context); + Log.i(TAG, "reinitializeClientRecordInDictionaryContentProvider() : MetadataFileUri = " + + metadataFileUri); final String metadataAdditionalId = MetadataFileUriGetter.getMetadataAdditionalId(context); // Tell the content provider to reset all information about this client id final Uri metadataContentUri = getProviderUriBuilder(clientId) @@ -501,9 +517,34 @@ public final class BinaryDictionaryFileDumper { final int length = dictionaryList.size(); for (int i = 0; i < length; ++i) { final DictionaryInfo info = dictionaryList.get(i); + Log.i(TAG, "reinitializeClientRecordInDictionaryContentProvider() : Insert " + info); client.insert(Uri.withAppendedPath(dictionaryContentUriBase, info.mId), info.toContentValues()); } + + // Read from metadata file in resources to get the baseline dictionary info. + // This ensures we start with a sane list of available dictionaries. + final int metadataResourceId = context.getResources().getIdentifier("metadata", + "raw", DictionaryInfoUtils.RESOURCE_PACKAGE_NAME); + if (metadataResourceId == 0) { + Log.w(TAG, "Missing metadata.json resource"); + return; + } + InputStream inputStream = null; + try { + inputStream = context.getResources().openRawResource(metadataResourceId); + UpdateHandler.handleMetadata(context, inputStream, clientId); + } catch (Exception e) { + Log.w(TAG, "Failed to read metadata.json from resources", e); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + Log.w(TAG, "Failed to close metadata.json", e); + } + } + } } /** diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java index 5f2a112ba..60016371b 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java @@ -195,39 +195,6 @@ final public class BinaryDictionaryGetter { return result; } - /** - * Remove all files with the passed id, except the passed file. - * - * If a dictionary with a given ID has a metadata change that causes it to change - * path, we need to remove the old version. The only way to do this is to check all - * installed files for a matching ID in a different directory. - */ - public static void removeFilesWithIdExcept(final Context context, final String id, - final File fileToKeep) { - try { - final File canonicalFileToKeep = fileToKeep.getCanonicalFile(); - final File[] directoryList = DictionaryInfoUtils.getCachedDirectoryList(context); - if (null == directoryList) return; - for (File directory : directoryList) { - // There is one directory per locale. See #getCachedDirectoryList - if (!directory.isDirectory()) continue; - final File[] wordLists = directory.listFiles(); - if (null == wordLists) continue; - for (File wordList : wordLists) { - final String fileId = - DictionaryInfoUtils.getWordListIdFromFileName(wordList.getName()); - if (fileId.equals(id)) { - if (!canonicalFileToKeep.equals(wordList.getCanonicalFile())) { - wordList.delete(); - } - } - } - } - } catch (java.io.IOException e) { - Log.e(TAG, "IOException trying to cleanup files", e); - } - } - // ## HACK ## we prevent usage of a dictionary before version 18. The reason for this is, since // those do not include whitelist entries, the new code with an old version of the dictionary // would lose whitelist functionality. @@ -274,12 +241,18 @@ final public class BinaryDictionaryGetter { */ public static ArrayList<AssetFileAddress> getDictionaryFiles(final Locale locale, final Context context, boolean notifyDictionaryPackForUpdates) { - - final boolean hasDefaultWordList = DictionaryInfoUtils.isDictionaryAvailable( - context, locale); if (notifyDictionaryPackForUpdates) { - BinaryDictionaryFileDumper.cacheWordListsFromContentProvider(locale, context, - hasDefaultWordList); + final boolean hasDefaultWordList = DictionaryInfoUtils.isDictionaryAvailable( + context, locale); + // It makes sure that the first time keyboard comes up and the dictionaries are reset, + // the DB is populated with the appropriate values for each locale. Helps in downloading + // the dictionaries when the user enables and switches new languages before the + // DictionaryService runs. + BinaryDictionaryFileDumper.downloadDictIfNeverRequested( + locale, context, hasDefaultWordList); + + // Move a staging files to the cache ddirectories if any. + DictionaryInfoUtils.moveStagingFilesIfExists(context); } final File[] cachedWordLists = getCachedWordLists(locale.toString(), context); final String mainDictId = DictionaryInfoUtils.getMainDictId(locale); diff --git a/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java b/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java index ff798abd6..02015da09 100644 --- a/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java +++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java @@ -17,6 +17,7 @@ package com.android.inputmethod.latin; import android.content.Context; +import android.util.LruCache; import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.keyboard.Keyboard; @@ -55,6 +56,18 @@ public interface DictionaryFacilitator { Dictionary.TYPE_USER}; /** + * The facilitator will put words into the cache whenever it decodes them. + * @param cache + */ + void setValidSpellingWordReadCache(final LruCache<String, Boolean> cache); + + /** + * The facilitator will get words from the cache whenever it needs to check their spelling. + * @param cache + */ + void setValidSpellingWordWriteCache(final LruCache<String, Boolean> cache); + + /** * Returns whether this facilitator is exactly for this locale. * * @param locale the locale to test against @@ -88,12 +101,16 @@ public interface DictionaryFacilitator { * * WARNING: The service methods that call start/finish are very spammy. */ - void onFinishInput(); + void onFinishInput(Context context); boolean isActive(); Locale getLocale(); + boolean usesContacts(); + + String getAccount(); + void resetDictionaries( final Context context, final Locale newLocale, @@ -149,7 +166,7 @@ public interface DictionaryFacilitator { boolean isValidSuggestionWord(final String word); - void clearUserHistoryDictionary(final Context context); + boolean clearUserHistoryDictionary(final Context context); String dump(final Context context); diff --git a/java/src/com/android/inputmethod/latin/DictionaryFacilitatorImpl.java b/java/src/com/android/inputmethod/latin/DictionaryFacilitatorImpl.java index 7233d27ab..c7115c9d9 100644 --- a/java/src/com/android/inputmethod/latin/DictionaryFacilitatorImpl.java +++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitatorImpl.java @@ -19,6 +19,7 @@ package com.android.inputmethod.latin; import android.content.Context; import android.text.TextUtils; import android.util.Log; +import android.util.LruCache; import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.keyboard.Keyboard; @@ -26,6 +27,7 @@ import com.android.inputmethod.latin.NgramContext.WordInfo; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.common.ComposedData; import com.android.inputmethod.latin.common.Constants; +import com.android.inputmethod.latin.common.StringUtils; import com.android.inputmethod.latin.personalization.UserHistoryDictionary; import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion; import com.android.inputmethod.latin.utils.ExecutorUtils; @@ -82,6 +84,19 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator { private static final Class<?>[] DICT_FACTORY_METHOD_ARG_TYPES = new Class[] { Context.class, Locale.class, File.class, String.class, String.class }; + private LruCache<String, Boolean> mValidSpellingWordReadCache; + private LruCache<String, Boolean> mValidSpellingWordWriteCache; + + @Override + public void setValidSpellingWordReadCache(final LruCache<String, Boolean> cache) { + mValidSpellingWordReadCache = cache; + } + + @Override + public void setValidSpellingWordWriteCache(final LruCache<String, Boolean> cache) { + mValidSpellingWordWriteCache = cache; + } + @Override public boolean isForLocale(final Locale locale) { return locale != null && locale.equals(mDictionaryGroup.mLocale); @@ -207,7 +222,7 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator { } @Override - public void onFinishInput() { + public void onFinishInput(Context context) { } @Override @@ -220,6 +235,16 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator { return mDictionaryGroup.mLocale; } + @Override + public boolean usesContacts() { + return mDictionaryGroup.getSubDict(Dictionary.TYPE_CONTACTS) != null; + } + + @Override + public String getAccount() { + return null; + } + @Nullable private static ExpandableBinaryDictionary getSubDict(final String dictType, final Context context, final Locale locale, final File dictFile, @@ -341,6 +366,10 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator { dictionarySetToCleanup.closeDict(dictType); } } + + if (mValidSpellingWordWriteCache != null) { + mValidSpellingWordWriteCache.evictAll(); + } } private void asyncReloadUninitializedMainDictionaries(final Context context, @@ -464,6 +493,10 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator { public void addToUserHistory(final String suggestion, final boolean wasAutoCapitalized, @Nonnull final NgramContext ngramContext, final long timeStampInSeconds, final boolean blockPotentiallyOffensive) { + // Update the spelling cache before learning. Words that are not yet added to user history + // and appear in no other language model are not considered valid. + putWordIntoValidSpellingWordCache("addToUserHistory", suggestion); + final String[] words = suggestion.split(Constants.WORD_SEPARATOR); NgramContext ngramContextForCurrentWord = ngramContext; for (int i = 0; i < words.length; i++) { @@ -477,6 +510,29 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator { } } + private void putWordIntoValidSpellingWordCache( + @Nonnull final String caller, + @Nonnull final String originalWord) { + if (mValidSpellingWordWriteCache == null) { + return; + } + + final String lowerCaseWord = originalWord.toLowerCase(getLocale()); + final boolean lowerCaseValid = isValidSpellingWord(lowerCaseWord); + mValidSpellingWordWriteCache.put(lowerCaseWord, lowerCaseValid); + + final String capitalWord = + StringUtils.capitalizeFirstAndDowncaseRest(originalWord, getLocale()); + final boolean capitalValid; + if (lowerCaseValid) { + // The lower case form of the word is valid, so the upper case must be valid. + capitalValid = true; + } else { + capitalValid = isValidSpellingWord(capitalWord); + } + mValidSpellingWordWriteCache.put(capitalWord, capitalValid); + } + private void addWordToUserHistory(final DictionaryGroup dictionaryGroup, final NgramContext ngramContext, final String word, final boolean wasAutoCapitalized, final int timeStampInSeconds, final boolean blockPotentiallyOffensive) { @@ -543,6 +599,10 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator { if (eventType != Constants.EVENT_BACKSPACE) { removeWord(Dictionary.TYPE_USER_HISTORY, word); } + + // Update the spelling cache after unlearning. Words that are removed from user history + // and appear in no other language model are not considered valid. + putWordIntoValidSpellingWordCache("unlearnFromUserHistory", word.toLowerCase()); } // TODO: Revise the way to fusion suggestion results. @@ -577,6 +637,13 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator { } public boolean isValidSpellingWord(final String word) { + if (mValidSpellingWordReadCache != null) { + final Boolean cachedValue = mValidSpellingWordReadCache.get(word); + if (cachedValue != null) { + return cachedValue; + } + } + return isValidWord(word, ALL_DICTIONARY_TYPES); } @@ -620,16 +687,18 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator { return maxFreq; } - private void clearSubDictionary(final String dictName) { + private boolean clearSubDictionary(final String dictName) { final ExpandableBinaryDictionary dictionary = mDictionaryGroup.getSubDict(dictName); - if (dictionary != null) { - dictionary.clear(); + if (dictionary == null) { + return false; } + dictionary.clear(); + return true; } @Override - public void clearUserHistoryDictionary(final Context context) { - clearSubDictionary(Dictionary.TYPE_USER_HISTORY); + public boolean clearUserHistoryDictionary(final Context context) { + return clearSubDictionary(Dictionary.TYPE_USER_HISTORY); } @Override diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index 330be377b..089670ebf 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -972,7 +972,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen void onFinishInputInternal() { super.onFinishInput(); - mDictionaryFacilitator.onFinishInput(); + mDictionaryFacilitator.onFinishInput(this); final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); if (mainKeyboardView != null) { mainKeyboardView.closing(); diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java index a123d282b..a10f2bdb0 100644 --- a/java/src/com/android/inputmethod/latin/RichInputConnection.java +++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java @@ -16,11 +16,10 @@ package com.android.inputmethod.latin; -import static com.android.inputmethod.latin.define.DecoderSpecificConstants.DICTIONARY_MAX_WORD_LENGTH; - import android.inputmethodservice.InputMethodService; import android.os.Build; import android.os.Bundle; +import android.os.SystemClock; import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.text.style.CharacterStyle; @@ -37,7 +36,6 @@ import com.android.inputmethod.compat.InputConnectionCompatUtils; import com.android.inputmethod.latin.common.Constants; import com.android.inputmethod.latin.common.UnicodeSurrogate; import com.android.inputmethod.latin.common.StringUtils; -import com.android.inputmethod.latin.define.DecoderSpecificConstants; import com.android.inputmethod.latin.inputlogic.PrivateCommandPerformer; import com.android.inputmethod.latin.settings.SpacingAndPunctuations; import com.android.inputmethod.latin.utils.CapsModeUtils; @@ -45,8 +43,11 @@ import com.android.inputmethod.latin.utils.DebugLogUtils; import com.android.inputmethod.latin.utils.NgramContextUtils; import com.android.inputmethod.latin.utils.ScriptUtils; import com.android.inputmethod.latin.utils.SpannableStringUtils; +import com.android.inputmethod.latin.utils.StatsUtils; import com.android.inputmethod.latin.utils.TextRange; +import java.util.concurrent.TimeUnit; + import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -59,17 +60,42 @@ import javax.annotation.Nullable; * for example. */ public final class RichInputConnection implements PrivateCommandPerformer { - private static final String TAG = RichInputConnection.class.getSimpleName(); + private static final String TAG = "RichInputConnection"; private static final boolean DBG = false; private static final boolean DEBUG_PREVIOUS_TEXT = false; private static final boolean DEBUG_BATCH_NESTING = false; - // Provision for realistic N-grams like "Hello, how are you?" and "I'm running 5 late". - // Technically, this will not handle 5-grams composed of long words, but in practice, - // our language models don't include that much data. - private static final int LOOKBACK_CHARACTER_NUM = 80; + private static final int NUM_CHARS_TO_GET_BEFORE_CURSOR = 40; + private static final int NUM_CHARS_TO_GET_AFTER_CURSOR = 40; private static final int INVALID_CURSOR_POSITION = -1; /** + * The amount of time a {@link #reloadTextCache} call needs to take for the keyboard to enter + * the {@link #hasSlowInputConnection} state. + */ + private static final long SLOW_INPUT_CONNECTION_ON_FULL_RELOAD_MS = 1000; + /** + * The amount of time a {@link #getTextBeforeCursor} or {@link #getTextAfterCursor} call needs + * to take for the keyboard to enter the {@link #hasSlowInputConnection} state. + */ + private static final long SLOW_INPUT_CONNECTION_ON_PARTIAL_RELOAD_MS = 200; + + private static final int OPERATION_GET_TEXT_BEFORE_CURSOR = 0; + private static final int OPERATION_GET_TEXT_AFTER_CURSOR = 1; + private static final int OPERATION_GET_WORD_RANGE_AT_CURSOR = 2; + private static final int OPERATION_RELOAD_TEXT_CACHE = 3; + private static final String[] OPERATION_NAMES = new String[] { + "GET_TEXT_BEFORE_CURSOR", + "GET_TEXT_AFTER_CURSOR", + "GET_WORD_RANGE_AT_CURSOR", + "RELOAD_TEXT_CACHE"}; + + /** + * The amount of time the keyboard will persist in the {@link #hasSlowInputConnection} state + * after observing a slow InputConnection event. + */ + private static final long SLOW_INPUTCONNECTION_PERSIST_MS = TimeUnit.MINUTES.toMillis(10); + + /** * This variable contains an expected value for the selection start position. This is where the * cursor or selection start may end up after all the keyboard-triggered updates have passed. We * keep this to compare it to the actual selection start to guess whether the move was caused by @@ -85,7 +111,7 @@ public final class RichInputConnection implements PrivateCommandPerformer { private int mExpectedSelEnd = INVALID_CURSOR_POSITION; // in chars, not code points /** * This contains the committed text immediately preceding the cursor and the composing - * text if any. It is refreshed when the cursor moves by calling upon the TextView. + * text, if any. It is refreshed when the cursor moves by calling upon the TextView. */ private final StringBuilder mCommittedTextBeforeComposingText = new StringBuilder(); /** @@ -100,8 +126,13 @@ public final class RichInputConnection implements PrivateCommandPerformer { private SpannableStringBuilder mTempObjectForCommitText = new SpannableStringBuilder(); private final InputMethodService mParent; - InputConnection mIC; - int mNestLevel; + private InputConnection mIC; + private int mNestLevel; + + /** + * The timestamp of the last slow InputConnection operation + */ + private long mLastSlowInputConnectionTime = -SLOW_INPUTCONNECTION_PERSIST_MS; public RichInputConnection(final InputMethodService parent) { mParent = parent; @@ -113,6 +144,19 @@ public final class RichInputConnection implements PrivateCommandPerformer { return mIC != null; } + /** + * Returns whether or not the underlying InputConnection is slow. When true, we want to avoid + * calling InputConnection methods that trigger an IPC round-trip (e.g., getTextAfterCursor). + */ + public boolean hasSlowInputConnection() { + return (SystemClock.uptimeMillis() - mLastSlowInputConnectionTime) + <= SLOW_INPUTCONNECTION_PERSIST_MS; + } + + public void onStartInput() { + mLastSlowInputConnectionTime = -SLOW_INPUTCONNECTION_PERSIST_MS; + } + private void checkConsistencyForDebug() { final ExtractedTextRequest r = new ExtractedTextRequest(); r.hintMaxChars = 0; @@ -211,9 +255,11 @@ public final class RichInputConnection implements PrivateCommandPerformer { mIC = mParent.getCurrentInputConnection(); // Call upon the inputconnection directly since our own method is using the cache, and // we want to refresh it. - final CharSequence textBeforeCursor = isConnected() - ? mIC.getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE, 0) - : null; + final CharSequence textBeforeCursor = getTextBeforeCursorAndDetectLaggyConnection( + OPERATION_RELOAD_TEXT_CACHE, + SLOW_INPUT_CONNECTION_ON_FULL_RELOAD_MS, + Constants.EDITOR_CONTENTS_CACHE_SIZE, + 0 /* flags */); if (null == textBeforeCursor) { // For some reason the app thinks we are not connected to it. This looks like a // framework bug... Fall back to ground state and return false. @@ -377,16 +423,54 @@ public final class RichInputConnection implements PrivateCommandPerformer { } return s; } + return getTextBeforeCursorAndDetectLaggyConnection( + OPERATION_GET_TEXT_BEFORE_CURSOR, + SLOW_INPUT_CONNECTION_ON_PARTIAL_RELOAD_MS, + n, flags); + } + + private CharSequence getTextBeforeCursorAndDetectLaggyConnection( + final int operation, final long timeout, final int n, final int flags) { mIC = mParent.getCurrentInputConnection(); - return isConnected() ? mIC.getTextBeforeCursor(n, flags) : null; + if (!isConnected()) { + return null; + } + final long startTime = SystemClock.uptimeMillis(); + final CharSequence result = mIC.getTextBeforeCursor(n, flags); + detectLaggyConnection(operation, timeout, startTime); + return result; } public CharSequence getTextAfterCursor(final int n, final int flags) { + return getTextAfterCursorAndDetectLaggyConnection( + OPERATION_GET_TEXT_AFTER_CURSOR, + SLOW_INPUT_CONNECTION_ON_PARTIAL_RELOAD_MS, + n, flags); + } + + private CharSequence getTextAfterCursorAndDetectLaggyConnection( + final int operation, final long timeout, final int n, final int flags) { mIC = mParent.getCurrentInputConnection(); - return isConnected() ? mIC.getTextAfterCursor(n, flags) : null; + if (!isConnected()) { + return null; + } + final long startTime = SystemClock.uptimeMillis(); + final CharSequence result = mIC.getTextAfterCursor(n, flags); + detectLaggyConnection(operation, timeout, startTime); + return result; + } + + private void detectLaggyConnection(final int operation, final long timeout, final long startTime) { + final long duration = SystemClock.uptimeMillis() - startTime; + if (duration >= timeout) { + final String operationName = OPERATION_NAMES[operation]; + Log.w(TAG, "Slow InputConnection: " + operationName + " took " + duration + " ms."); + StatsUtils.onInputConnectionLaggy(operation, duration); + mLastSlowInputConnectionTime = SystemClock.uptimeMillis(); + } } - public void deleteSurroundingText(final int beforeLength, final int afterLength) { + public void deleteTextBeforeCursor(final int beforeLength) { if (DEBUG_BATCH_NESTING) checkBatchEdit(); // TODO: the following is incorrect if the cursor is not immediately after the composition. // Right now we never come here in this case because we reset the composing state before we @@ -411,7 +495,7 @@ public final class RichInputConnection implements PrivateCommandPerformer { mExpectedSelStart = 0; } if (isConnected()) { - mIC.deleteSurroundingText(beforeLength, afterLength); + mIC.deleteSurroundingText(beforeLength, 0); } if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); } @@ -576,9 +660,9 @@ public final class RichInputConnection implements PrivateCommandPerformer { if (!isConnected()) { return NgramContext.EMPTY_PREV_WORDS_INFO; } - final CharSequence prev = getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0); + final CharSequence prev = getTextBeforeCursor(NUM_CHARS_TO_GET_BEFORE_CURSOR, 0); if (DEBUG_PREVIOUS_TEXT && null != prev) { - final int checkLength = LOOKBACK_CHARACTER_NUM - 1; + final int checkLength = NUM_CHARS_TO_GET_BEFORE_CURSOR - 1; final String reference = prev.length() <= checkLength ? prev.toString() : prev.subSequence(prev.length() - checkLength, prev.length()).toString(); // TODO: right now the following works because mComposingText holds the part of the @@ -621,9 +705,15 @@ public final class RichInputConnection implements PrivateCommandPerformer { if (!isConnected()) { return null; } - final CharSequence before = mIC.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, + final CharSequence before = getTextBeforeCursorAndDetectLaggyConnection( + OPERATION_GET_WORD_RANGE_AT_CURSOR, + SLOW_INPUT_CONNECTION_ON_PARTIAL_RELOAD_MS, + NUM_CHARS_TO_GET_BEFORE_CURSOR, InputConnection.GET_TEXT_WITH_STYLES); - final CharSequence after = mIC.getTextAfterCursor(LOOKBACK_CHARACTER_NUM, + final CharSequence after = getTextAfterCursorAndDetectLaggyConnection( + OPERATION_GET_WORD_RANGE_AT_CURSOR, + SLOW_INPUT_CONNECTION_ON_PARTIAL_RELOAD_MS, + NUM_CHARS_TO_GET_AFTER_CURSOR, InputConnection.GET_TEXT_WITH_STYLES); if (before == null || after == null) { return null; @@ -666,8 +756,9 @@ public final class RichInputConnection implements PrivateCommandPerformer { hasUrlSpans); } - public boolean isCursorTouchingWord(final SpacingAndPunctuations spacingAndPunctuations) { - if (isCursorFollowedByWordCharacter(spacingAndPunctuations)) { + public boolean isCursorTouchingWord(final SpacingAndPunctuations spacingAndPunctuations, + boolean checkTextAfter) { + if (checkTextAfter && isCursorFollowedByWordCharacter(spacingAndPunctuations)) { // If what's after the cursor is a word character, then we're touching a word. return true; } @@ -704,7 +795,7 @@ public final class RichInputConnection implements PrivateCommandPerformer { if (DEBUG_BATCH_NESTING) checkBatchEdit(); final int codePointBeforeCursor = getCodePointBeforeCursor(); if (Constants.CODE_SPACE == codePointBeforeCursor) { - deleteSurroundingText(1, 0); + deleteTextBeforeCursor(1); } } @@ -730,7 +821,7 @@ public final class RichInputConnection implements PrivateCommandPerformer { } // Double-space results in ". ". A backspace to cancel this should result in a single // space in the text field, so we replace ". " with a single space. - deleteSurroundingText(2, 0); + deleteTextBeforeCursor(2); final String singleSpace = " "; commitText(singleSpace, 1); return true; @@ -752,7 +843,7 @@ public final class RichInputConnection implements PrivateCommandPerformer { + "find a space just before the cursor."); return false; } - deleteSurroundingText(2, 0); + deleteTextBeforeCursor(2); final String text = " " + textBeforeCursor.subSequence(0, 1); commitText(text, 1); return true; diff --git a/java/src/com/android/inputmethod/latin/SystemBroadcastReceiver.java b/java/src/com/android/inputmethod/latin/SystemBroadcastReceiver.java index 0d081e0d2..90221512f 100644 --- a/java/src/com/android/inputmethod/latin/SystemBroadcastReceiver.java +++ b/java/src/com/android/inputmethod/latin/SystemBroadcastReceiver.java @@ -16,6 +16,7 @@ package com.android.inputmethod.latin; +import android.app.DownloadManager; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; @@ -23,14 +24,15 @@ import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; +import android.database.Cursor; import android.os.Process; import android.preference.PreferenceManager; import android.util.Log; import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodSubtype; -import com.android.inputmethod.dictionarypack.CommonPreferences; import com.android.inputmethod.dictionarypack.DictionaryPackConstants; +import com.android.inputmethod.dictionarypack.DownloadManagerWrapper; import com.android.inputmethod.keyboard.KeyboardLayoutSet; import com.android.inputmethod.latin.settings.Settings; import com.android.inputmethod.latin.setup.SetupActivity; @@ -75,7 +77,12 @@ public final class SystemBroadcastReceiver extends BroadcastReceiver { final InputMethodSubtype[] additionalSubtypes = richImm.getAdditionalSubtypes(); richImm.setAdditionalInputMethodSubtypes(additionalSubtypes); toggleAppIcon(context); - downloadLatestDictionaries(context); + + // Remove all the previously scheduled downloads. This will also makes sure + // that any erroneously stuck downloads will get cleared. (b/21797386) + removeOldDownloads(context); + // b/21797386 + // downloadLatestDictionaries(context); } else if (Intent.ACTION_BOOT_COMPLETED.equals(intentAction)) { Log.i(TAG, "Boot has been completed"); toggleAppIcon(context); @@ -103,13 +110,39 @@ public final class SystemBroadcastReceiver extends BroadcastReceiver { } } + private void removeOldDownloads(Context context) { + try { + Log.i(TAG, "Removing the old downloads in progress of the previous keyboard version."); + final DownloadManagerWrapper downloadManagerWrapper = new DownloadManagerWrapper( + context); + final DownloadManager.Query q = new DownloadManager.Query(); + // Query all the download statuses except the succeeded ones. + q.setFilterByStatus(DownloadManager.STATUS_FAILED + | DownloadManager.STATUS_PAUSED + | DownloadManager.STATUS_PENDING + | DownloadManager.STATUS_RUNNING); + final Cursor c = downloadManagerWrapper.query(q); + if (c != null) { + for (c.moveToFirst(); !c.isAfterLast(); c.moveToNext()) { + final long downloadId = c + .getLong(c.getColumnIndex(DownloadManager.COLUMN_ID)); + downloadManagerWrapper.remove(downloadId); + Log.i(TAG, "Removed the download with Id: " + downloadId); + } + c.close(); + } + } catch (Exception e) { + Log.e(TAG, "Exception while removing old downloads."); + } + } + private void downloadLatestDictionaries(Context context) { final Intent updateIntent = new Intent( DictionaryPackConstants.INIT_AND_UPDATE_NOW_INTENT_ACTION); context.sendBroadcast(updateIntent); } - private static void toggleAppIcon(final Context context) { + public static void toggleAppIcon(final Context context) { final int appInfoFlags = context.getApplicationInfo().flags; final boolean isSystemApp = (appInfoFlags & ApplicationInfo.FLAG_SYSTEM) > 0; if (Log.isLoggable(TAG, Log.INFO)) { diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java index f7dbc0a4d..1dd5850f8 100644 --- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java +++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java @@ -139,6 +139,7 @@ public final class InputLogic { public void startInput(final String combiningSpec, final SettingsValues settingsValues) { mEnteredText = null; mWordBeingCorrectedByCursor = null; + mConnection.onStartInput(); 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. @@ -398,9 +399,8 @@ public final class InputLogic { 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); + performAdditionToUserHistoryDictionary(settingsValues, mWordBeingCorrectedByCursor, + NgramContext.EMPTY_PREV_WORDS_INFO); } } else { // resetEntireInputState calls resetCachesUponCursorMove, but forcing the @@ -473,7 +473,7 @@ public final class InputLogic { } // Try to record the word being corrected when the user enters a word character or // the backspace key. - if (!mWordComposer.isComposingWord() + if (!mConnection.hasSlowInputConnection() && !mWordComposer.isComposingWord() && (settingsValues.isWordCodePoint(processedEvent.mCodePoint) || processedEvent.mKeyCode == Constants.CODE_DELETE)) { mWordBeingCorrectedByCursor = getWordAtCursor( @@ -833,8 +833,14 @@ public final class InputLogic { && settingsValues.needsToLookupSuggestions() && // In languages with spaces, we only start composing a word when we are not already // touching a word. In languages without spaces, the above conditions are sufficient. - (!mConnection.isCursorTouchingWord(settingsValues.mSpacingAndPunctuations) - || !settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces)) { + // NOTE: If the InputConnection is slow, we skip the text-after-cursor check since it + // can incur a very expensive getTextAfterCursor() lookup, potentially making the + // keyboard UI slow and non-responsive. + // TODO: Cache the text after the cursor so we don't need to go to the InputConnection + // each time. We are already doing this for getTextBeforeCursor(). + (!settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces + || !mConnection.isCursorTouchingWord(settingsValues.mSpacingAndPunctuations, + !mConnection.hasSlowInputConnection() /* checkTextAfter */))) { // Reset entirely the composing state anyway, then start composing a new word unless // the character is a word connector. The idea here is, word connectors are not // separators and they should be treated as normal characters, except in the first @@ -1054,7 +1060,7 @@ public final class InputLogic { // Cancel multi-character input: remove the text we just entered. // This is triggered on backspace after a key that inputs multiple characters, // like the smiley key or the .com key. - mConnection.deleteSurroundingText(mEnteredText.length(), 0); + mConnection.deleteTextBeforeCursor(mEnteredText.length()); StatsUtils.onDeleteMultiCharInput(mEnteredText.length()); mEnteredText = null; // If we have mEnteredText, then we know that mHasUncommittedTypedChars == false. @@ -1099,7 +1105,7 @@ public final class InputLogic { - mConnection.getExpectedSelectionStart(); mConnection.setSelection(mConnection.getExpectedSelectionEnd(), mConnection.getExpectedSelectionEnd()); - mConnection.deleteSurroundingText(numCharsDeleted, 0); + mConnection.deleteTextBeforeCursor(numCharsDeleted); StatsUtils.onBackspaceSelectedText(numCharsDeleted); } else { // There is no selection, just delete one character. @@ -1139,13 +1145,13 @@ public final class InputLogic { // broken apps expect something to happen in this case so that they can // catch it and have their broken interface react. If you need the keyboard // to do this, you're doing it wrong -- please fix your app. - mConnection.deleteSurroundingText(1, 0); + mConnection.deleteTextBeforeCursor(1); // TODO: Add a new StatsUtils method onBackspaceWhenNoText() return; } final int lengthToDelete = Character.isSupplementaryCodePoint(codePointBeforeCursor) ? 2 : 1; - mConnection.deleteSurroundingText(lengthToDelete, 0); + mConnection.deleteTextBeforeCursor(lengthToDelete); int totalDeletedLength = lengthToDelete; if (mDeleteCount > Constants.DELETE_ACCELERATE_AT) { // If this is an accelerated (i.e., double) deletion, then we need to @@ -1158,7 +1164,7 @@ public final class InputLogic { if (codePointBeforeCursorToDeleteAgain != Constants.NOT_A_CODE) { final int lengthToDeleteAgain = Character.isSupplementaryCodePoint( codePointBeforeCursorToDeleteAgain) ? 2 : 1; - mConnection.deleteSurroundingText(lengthToDeleteAgain, 0); + mConnection.deleteTextBeforeCursor(lengthToDeleteAgain); totalDeletedLength += lengthToDeleteAgain; } } @@ -1170,7 +1176,9 @@ public final class InputLogic { unlearnWordBeingDeleted( inputTransaction.mSettingsValues, currentKeyboardScriptId); } - if (inputTransaction.mSettingsValues.isSuggestionsEnabledPerUserSettings() + if (mConnection.hasSlowInputConnection()) { + mSuggestionStripViewAccessor.setNeutralSuggestionStrip(); + } else if (inputTransaction.mSettingsValues.isSuggestionsEnabledPerUserSettings() && inputTransaction.mSettingsValues.mSpacingAndPunctuations .mCurrentLanguageHasSpaces && !mConnection.isCursorFollowedByWordCharacter( @@ -1197,6 +1205,13 @@ public final class InputLogic { boolean unlearnWordBeingDeleted( final SettingsValues settingsValues, final int currentKeyboardScriptId) { + if (mConnection.hasSlowInputConnection()) { + // TODO: Refactor unlearning so that it does not incur any extra calls + // to the InputConnection. That way it can still be performed on a slow + // InputConnection. + Log.w(TAG, "Skipping unlearning due to slow InputConnection."); + return false; + } // 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. @@ -1242,7 +1257,7 @@ public final class InputLogic { if (Constants.CODE_SPACE != codePointBeforeCursor) { return false; } - mConnection.deleteSurroundingText(1, 0); + mConnection.deleteTextBeforeCursor(1); final String text = event.getTextToCommit() + " "; mConnection.commitText(text, 1); inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW); @@ -1332,7 +1347,7 @@ public final class InputLogic { Character.codePointAt(lastTwo, length - 3) : lastTwo.charAt(length - 2); if (canBeFollowedByDoubleSpacePeriod(firstCodePoint)) { cancelDoubleSpacePeriodCountdown(); - mConnection.deleteSurroundingText(1, 0); + mConnection.deleteTextBeforeCursor(1); final String textToInsert = inputTransaction.mSettingsValues.mSpacingAndPunctuations .mSentenceSeparatorAndSpace; mConnection.commitText(textToInsert, 1); @@ -1400,7 +1415,7 @@ public final class InputLogic { mConnection.finishComposingText(); mRecapitalizeStatus.rotate(); mConnection.setSelection(selectionEnd, selectionEnd); - mConnection.deleteSurroundingText(numCharsSelected, 0); + mConnection.deleteTextBeforeCursor(numCharsSelected); mConnection.commitText(mRecapitalizeStatus.getRecapitalizedString(), 0); mConnection.setSelection(mRecapitalizeStatus.getNewCursorStart(), mRecapitalizeStatus.getNewCursorEnd()); @@ -1412,6 +1427,12 @@ public final class InputLogic { // That's to avoid unintended additions in some sensitive fields, or fields that // expect to receive non-words. if (!settingsValues.mAutoCorrectionEnabledPerUserSettings) return; + if (mConnection.hasSlowInputConnection()) { + // Since we don't unlearn when the user backspaces on a slow InputConnection, + // turn off learning to guard against adding typos that the user later deletes. + Log.w(TAG, "Skipping learning due to slow InputConnection."); + return; + } if (TextUtils.isEmpty(suggestion)) return; final boolean wasAutoCapitalized = @@ -1515,7 +1536,8 @@ public final class InputLogic { return; } final int expectedCursorPosition = mConnection.getExpectedSelectionStart(); - if (!mConnection.isCursorTouchingWord(settingsValues.mSpacingAndPunctuations)) { + if (!mConnection.isCursorTouchingWord(settingsValues.mSpacingAndPunctuations, + true /* checkTextAfter */)) { // Show predictions. mWordComposer.setCapitalizedModeAtStartComposingTime(WordComposer.CAPS_MODE_OFF); mLatinIME.mHandler.postUpdateSuggestionStrip(SuggestedWords.INPUT_STYLE_RECORRECTION); @@ -1638,7 +1660,7 @@ public final class InputLogic { + "\", but before the cursor we found \"" + wordBeforeCursor + "\""); } } - mConnection.deleteSurroundingText(deleteLength, 0); + mConnection.deleteTextBeforeCursor(deleteLength); if (!TextUtils.isEmpty(committedWord)) { unlearnWord(committedWordString, inputTransaction.mSettingsValues, Constants.EVENT_REVERT); @@ -2136,9 +2158,10 @@ public final class InputLogic { final SuggestedWords suggestedWords = mSuggestedWords; // TODO: Locale should be determined based on context and the text given. final Locale locale = getDictionaryFacilitatorLocale(); - final CharSequence chosenWordWithSuggestions = - SuggestionSpanUtils.getTextWithSuggestionSpan(mLatinIME, chosenWord, - suggestedWords, locale); + final CharSequence chosenWordWithSuggestions = chosenWord; + // b/21926256 + // SuggestionSpanUtils.getTextWithSuggestionSpan(mLatinIME, chosenWord, + // suggestedWords, locale); if (DebugFlags.DEBUG_ENABLED) { long runTimeMillis = System.currentTimeMillis() - startTimeMillis; Log.d(TAG, "commitChosenWord() : " + runTimeMillis + " ms to run " diff --git a/java/src/com/android/inputmethod/latin/settings/AdvancedSettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/AdvancedSettingsFragment.java index f2e1aed4c..a6fb7f1f1 100644 --- a/java/src/com/android/inputmethod/latin/settings/AdvancedSettingsFragment.java +++ b/java/src/com/android/inputmethod/latin/settings/AdvancedSettingsFragment.java @@ -26,6 +26,7 @@ import android.preference.Preference; import com.android.inputmethod.latin.AudioAndHapticFeedbackManager; import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.SystemBroadcastReceiver; import com.android.inputmethod.latin.define.ProductionFlags; /** @@ -106,6 +107,8 @@ public final class AdvancedSettingsFragment extends SubScreenFragment { if (key.equals(Settings.PREF_POPUP_ON)) { setPreferenceEnabled(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY, Settings.readKeyPreviewPopupEnabled(prefs, res)); + } else if (key.equals(Settings.PREF_SHOW_SETUP_WIZARD_ICON)) { + SystemBroadcastReceiver.toggleAppIcon(getActivity()); } updateListPreferenceSummaryToCurrentValue(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY); refreshEnablingsOfKeypressSoundAndVibrationSettings(); diff --git a/java/src/com/android/inputmethod/latin/settings/CorrectionSettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/CorrectionSettingsFragment.java index aa73a9a83..d28e703fe 100644 --- a/java/src/com/android/inputmethod/latin/settings/CorrectionSettingsFragment.java +++ b/java/src/com/android/inputmethod/latin/settings/CorrectionSettingsFragment.java @@ -16,20 +16,27 @@ package com.android.inputmethod.latin.settings; +import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; import android.os.Build; import android.os.Bundle; import android.preference.Preference; import com.android.inputmethod.dictionarypack.DictionarySettingsActivity; import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.userdictionary.UserDictionaryList; +import com.android.inputmethod.latin.userdictionary.UserDictionarySettings; + +import java.util.TreeSet; /** * "Text correction" settings sub screen. * * This settings sub screen handles the following text correction preferences. + * - Personal dictionary * - Add-on dictionaries * - Block offensive words * - Auto-correction @@ -59,5 +66,39 @@ public final class CorrectionSettingsFragment extends SubScreenFragment { if (0 >= number) { removePreference(Settings.PREF_CONFIGURE_DICTIONARIES_KEY); } + + final Preference editPersonalDictionary = + findPreference(Settings.PREF_EDIT_PERSONAL_DICTIONARY); + final Intent editPersonalDictionaryIntent = editPersonalDictionary.getIntent(); + final ResolveInfo ri = USE_INTERNAL_PERSONAL_DICTIONARY_SETTINGS ? null + : pm.resolveActivity( + editPersonalDictionaryIntent, PackageManager.MATCH_DEFAULT_ONLY); + if (ri == null) { + overwriteUserDictionaryPreference(editPersonalDictionary); + } + } + + private void overwriteUserDictionaryPreference(final Preference userDictionaryPreference) { + final Activity activity = getActivity(); + final TreeSet<String> localeList = UserDictionaryList.getUserDictionaryLocalesSet(activity); + if (null == localeList) { + // The locale list is null if and only if the user dictionary service is + // not present or disabled. In this case we need to remove the preference. + getPreferenceScreen().removePreference(userDictionaryPreference); + } else if (localeList.size() <= 1) { + userDictionaryPreference.setFragment(UserDictionarySettings.class.getName()); + // If the size of localeList is 0, we don't set the locale parameter in the + // extras. This will be interpreted by the UserDictionarySettings class as + // meaning "the current locale". + // Note that with the current code for UserDictionaryList#getUserDictionaryLocalesSet() + // the locale list always has at least one element, since it always includes the current + // locale explicitly. @see UserDictionaryList.getUserDictionaryLocalesSet(). + if (localeList.size() == 1) { + final String locale = (String)localeList.toArray()[0]; + userDictionaryPreference.getExtras().putString("locale", locale); + } + } else { + userDictionaryPreference.setFragment(UserDictionaryList.class.getName()); + } } } diff --git a/java/src/com/android/inputmethod/latin/settings/Settings.java b/java/src/com/android/inputmethod/latin/settings/Settings.java index 694f43d3f..940f1bdfc 100644 --- a/java/src/com/android/inputmethod/latin/settings/Settings.java +++ b/java/src/com/android/inputmethod/latin/settings/Settings.java @@ -56,6 +56,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang // PREF_VOICE_MODE_OBSOLETE is obsolete. Use PREF_VOICE_INPUT_KEY instead. public static final String PREF_VOICE_MODE_OBSOLETE = "voice_mode"; public static final String PREF_VOICE_INPUT_KEY = "pref_voice_input_key"; + public static final String PREF_EDIT_PERSONAL_DICTIONARY = "edit_personal_dictionary"; public static final String PREF_CONFIGURE_DICTIONARIES_KEY = "configure_dictionaries_key"; // PREF_AUTO_CORRECTION_THRESHOLD_OBSOLETE is obsolete. Use PREF_AUTO_CORRECTION instead. public static final String PREF_AUTO_CORRECTION_THRESHOLD_OBSOLETE = diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java index 2c690aea7..c7622e7a1 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java @@ -84,8 +84,7 @@ public final class AndroidSpellCheckerSession extends AndroidWordLevelSpellCheck if (TextUtils.isEmpty(splitText)) { continue; } - if (mSuggestionsCache.getSuggestionsFromCache(splitText.toString(), ngramContext) - == null) { + if (mSuggestionsCache.getSuggestionsFromCache(splitText.toString()) == null) { continue; } final int newLength = splitText.length(); diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java index 1322ce240..9223923a7 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java @@ -71,30 +71,26 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { } protected static final class SuggestionsCache { - private static final char CHAR_DELIMITER = '\uFFFC'; private static final int MAX_CACHE_SIZE = 50; private final LruCache<String, SuggestionsParams> mUnigramSuggestionsInfoCache = new LruCache<>(MAX_CACHE_SIZE); - private static String generateKey(final String query, final NgramContext ngramContext) { - if (TextUtils.isEmpty(query) || !ngramContext.isValid()) { - return query; - } - return query + CHAR_DELIMITER + ngramContext; + private static String generateKey(final String query) { + return query + ""; } - public SuggestionsParams getSuggestionsFromCache(String query, - final NgramContext ngramContext) { - return mUnigramSuggestionsInfoCache.get(generateKey(query, ngramContext)); + public SuggestionsParams getSuggestionsFromCache(final String query) { + return mUnigramSuggestionsInfoCache.get(query); } - public void putSuggestionsToCache(final String query, final NgramContext ngramContext, - final String[] suggestions, final int flags) { + public void putSuggestionsToCache( + final String query, final String[] suggestions, final int flags) { if (suggestions == null || TextUtils.isEmpty(query)) { return; } mUnigramSuggestionsInfoCache.put( - generateKey(query, ngramContext), new SuggestionsParams(suggestions, flags)); + generateKey(query), + new SuggestionsParams(suggestions, flags)); } public void clearCache() { @@ -232,16 +228,7 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { AndroidSpellCheckerService.SINGLE_QUOTE). replaceAll("^" + quotesRegexp, ""). replaceAll(quotesRegexp + "$", ""); - final SuggestionsParams cachedSuggestionsParams = - mSuggestionsCache.getSuggestionsFromCache(text, ngramContext); - - if (cachedSuggestionsParams != null) { - Log.d(TAG, "onGetSuggestionsInternal() : Cache hit for [" + text + "]"); - return new SuggestionsInfo( - cachedSuggestionsParams.mFlags, cachedSuggestionsParams.mSuggestions); - } - // If spell checking is impossible, return early. if (!mService.hasMainDictionaryForLocale(mLocale)) { return AndroidSpellCheckerService.getNotInDictEmptySuggestions( false /* reportAsTypo */); @@ -329,8 +316,7 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { .getValueOf_RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS() : 0); final SuggestionsInfo retval = new SuggestionsInfo(flags, result.mSuggestions); - mSuggestionsCache.putSuggestionsToCache(text, ngramContext, result.mSuggestions, - flags); + mSuggestionsCache.putSuggestionsToCache(text, result.mSuggestions, flags); return retval; } catch (RuntimeException e) { // Don't kill the keyboard if there is a bug in the spell checker diff --git a/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java b/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java index abe4b28bc..cea2e13b1 100644 --- a/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java @@ -54,7 +54,7 @@ import javax.annotation.Nullable; */ public class DictionaryInfoUtils { private static final String TAG = DictionaryInfoUtils.class.getSimpleName(); - private static final String RESOURCE_PACKAGE_NAME = R.class.getPackage().getName(); + public static final String RESOURCE_PACKAGE_NAME = R.class.getPackage().getName(); private static final String DEFAULT_MAIN_DICT = "main"; private static final String MAIN_DICT_PREFIX = "main_"; private static final String DECODER_DICT_SUFFIX = DecoderSpecificConstants.DECODER_DICT_SUFFIX; @@ -240,7 +240,7 @@ public class DictionaryInfoUtils { /** * Find out the cache directory associated with a specific locale. */ - private static String getCacheDirectoryForLocale(final String locale, final Context context) { + public static String getCacheDirectoryForLocale(final String locale, final Context context) { final String relativeDirectoryName = replaceFileNameDangerousCharacters(locale); final String absoluteDirectoryName = getWordListCacheDirectory(context) + File.separator + relativeDirectoryName; |