diff options
Diffstat (limited to 'java/src/com/android/inputmethod')
12 files changed, 235 insertions, 191 deletions
diff --git a/java/src/com/android/inputmethod/latin/ContactsContentObserver.java b/java/src/com/android/inputmethod/latin/ContactsContentObserver.java index 019d17d56..e45681bd7 100644 --- a/java/src/com/android/inputmethod/latin/ContactsContentObserver.java +++ b/java/src/com/android/inputmethod/latin/ContactsContentObserver.java @@ -23,26 +23,26 @@ import android.os.SystemClock; import android.provider.ContactsContract.Contacts; import android.util.Log; -import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.latin.ContactsManager.ContactsChangedListener; import com.android.inputmethod.latin.utils.ExecutorUtils; import java.util.ArrayList; -import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicBoolean; /** - * A content observer that listens to updates to content provider {@link Contacts.CONTENT_URI}. + * A content observer that listens to updates to content provider {@link Contacts#CONTENT_URI}. */ -// TODO:add test -public class ContactsContentObserver { +public class ContactsContentObserver implements Runnable { private static final String TAG = ContactsContentObserver.class.getSimpleName(); private static final boolean DEBUG = false; - - private ContentObserver mObserver; + private static AtomicBoolean sRunning = new AtomicBoolean(false); private final Context mContext; private final ContactsManager mManager; + private ContentObserver mContentObserver; + private ContactsChangedListener mContactsChangedListener; + public ContactsContentObserver(final ContactsManager manager, final Context context) { mManager = manager; mContext = context; @@ -52,32 +52,36 @@ public class ContactsContentObserver { if (DEBUG) { Log.d(TAG, "Registered Contacts Content Observer"); } - mObserver = new ContentObserver(null /* handler */) { + mContactsChangedListener = listener; + mContentObserver = new ContentObserver(null /* handler */) { @Override public void onChange(boolean self) { - getBgExecutor().execute(new Runnable() { - @Override - public void run() { - if (haveContentsChanged()) { - if (DEBUG) { - Log.d(TAG, "Contacts have changed; notifying listeners"); - } - listener.onContactsChange(); - } - } - }); + ExecutorUtils.getExecutorForDynamicLanguageModelUpdate() + .execute(ContactsContentObserver.this); } }; final ContentResolver contentResolver = mContext.getContentResolver(); - contentResolver.registerContentObserver(Contacts.CONTENT_URI, true, mObserver); + contentResolver.registerContentObserver(Contacts.CONTENT_URI, true, mContentObserver); } - @UsedForTesting - private ExecutorService getBgExecutor() { - return ExecutorUtils.getExecutor("Check Contacts"); + @Override + public void run() { + if (!sRunning.compareAndSet(false /* expect */, true /* update */)) { + if (DEBUG) { + Log.d(TAG, "run() : Already running. Don't waste time checking again."); + } + return; + } + if (haveContentsChanged()) { + if (DEBUG) { + Log.d(TAG, "run() : Contacts have changed. Notifying listeners."); + } + mContactsChangedListener.onContactsChange(); + } + sRunning.set(false); } - private boolean haveContentsChanged() { + boolean haveContentsChanged() { final long startTime = SystemClock.uptimeMillis(); final int contactCount = mManager.getContactCount(); if (contactCount > ContactsDictionaryConstants.MAX_CONTACT_COUNT) { @@ -105,6 +109,6 @@ public class ContactsContentObserver { } public void unregister() { - mContext.getContentResolver().unregisterContentObserver(mObserver); + mContext.getContentResolver().unregisterContentObserver(mContentObserver); } } diff --git a/java/src/com/android/inputmethod/latin/ContactsManager.java b/java/src/com/android/inputmethod/latin/ContactsManager.java index dc5abd955..1fadc6f6f 100644 --- a/java/src/com/android/inputmethod/latin/ContactsManager.java +++ b/java/src/com/android/inputmethod/latin/ContactsManager.java @@ -34,7 +34,6 @@ import java.util.concurrent.atomic.AtomicInteger; * The manager provides an API for listening to meaning full updates by keeping a * measure of the current state of the content provider. */ -// TODO:Add test public class ContactsManager { private static final String TAG = ContactsManager.class.getSimpleName(); private static final boolean DEBUG = false; diff --git a/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java b/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java index c22dc287c..addc8f209 100644 --- a/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java +++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java @@ -150,7 +150,9 @@ public interface DictionaryFacilitator { @Nonnull final NgramContext ngramContext, final int timeStampInSeconds, final boolean blockPotentiallyOffensive); - void removeWordFromPersonalizedDicts(final String word); + void unlearnFromUserHistory(final String word, + @Nonnull final NgramContext ngramContext, final int timeStampInSeconds, + final int eventType); // TODO: Revise the way to fusion suggestion results. SuggestionResults getSuggestionResults(final WordComposer composer, @@ -171,12 +173,4 @@ public interface DictionaryFacilitator { void dumpDictionaryForDebug(final String dictName); ArrayList<Pair<String, DictionaryStats>> getStatsOfEnabledSubDicts(); - - void addOrIncrementTerm(String fileName, - String finalWordToBeAdded, - NgramContext ngramContext, - int increment, - int timeStampInSeconds); - - void clearLanguageModel(String filePath); } diff --git a/java/src/com/android/inputmethod/latin/DictionaryFacilitatorImpl.java b/java/src/com/android/inputmethod/latin/DictionaryFacilitatorImpl.java index 3d76751ce..1e0885420 100644 --- a/java/src/com/android/inputmethod/latin/DictionaryFacilitatorImpl.java +++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitatorImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013 The Android Open Source Project +7 * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -443,7 +443,7 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator { final Locale[] locales, final DictionaryInitializationListener listener) { final CountDownLatch latchForWaitingLoadingMainDictionary = new CountDownLatch(1); mLatchForWaitingLoadingMainDictionaries = latchForWaitingLoadingMainDictionary; - ExecutorUtils.getExecutor("InitializeBinaryDictionary").execute(new Runnable() { + ExecutorUtils.getExecutorForStaticLanguageModelUpdate().execute(new Runnable() { @Override public void run() { doReloadUninitializedMainDictionaries( @@ -654,8 +654,14 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator { } } - public void removeWordFromPersonalizedDicts(final String word) { - removeWord(Dictionary.TYPE_USER_HISTORY, word); + @Override + public void unlearnFromUserHistory(final String word, + @Nonnull final NgramContext ngramContext, final int timeStampInSeconds, + final int eventType) { + // TODO: Decide whether or not to remove the word on EVENT_BACKSPACE. + if (eventType != Constants.EVENT_BACKSPACE) { + removeWord(Dictionary.TYPE_USER_HISTORY, word); + } } // TODO: Revise the way to fusion suggestion results. @@ -766,10 +772,12 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator { } } + @Override public void clearUserHistoryDictionary() { clearSubDictionary(Dictionary.TYPE_USER_HISTORY); } + @Override public void dumpDictionaryForDebug(final String dictName) { final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { @@ -783,6 +791,7 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator { } } + @Override public ArrayList<Pair<String, DictionaryStats>> getStatsOfEnabledSubDicts() { final ArrayList<Pair<String, DictionaryStats>> statsOfEnabledSubDicts = new ArrayList<>(); final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; @@ -795,18 +804,4 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator { } return statsOfEnabledSubDicts; } - - @Override - public void addOrIncrementTerm(String fileName, - String word, - NgramContext ngramContext, - int increment, - int timeStampInSeconds) { - // Do nothing. - } - - @Override - public void clearLanguageModel(String filePath) { - // Do nothing. - } } diff --git a/java/src/com/android/inputmethod/latin/DictionaryFacilitatorLruCache.java b/java/src/com/android/inputmethod/latin/DictionaryFacilitatorLruCache.java index 85ecf93f3..b813af4c2 100644 --- a/java/src/com/android/inputmethod/latin/DictionaryFacilitatorLruCache.java +++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitatorLruCache.java @@ -16,15 +16,11 @@ package com.android.inputmethod.latin; -import java.util.HashSet; import java.util.Locale; import java.util.concurrent.TimeUnit; -import com.android.inputmethod.annotations.UsedForTesting; - import android.content.Context; import android.util.Log; -import android.util.LruCache; /** * Cache for dictionary facilitators of multiple locales. @@ -32,54 +28,20 @@ import android.util.LruCache; */ public class DictionaryFacilitatorLruCache { private static final String TAG = "DictionaryFacilitatorLruCache"; - private static final int MAX_DICTIONARY_FACILITATOR_COUNT = 3; private static final int WAIT_FOR_LOADING_MAIN_DICT_IN_MILLISECONDS = 1000; private static final int MAX_RETRY_COUNT_FOR_WAITING_FOR_LOADING_DICT = 5; - /** - * Class extends LruCache. This class tracks cached locales and closes evicted dictionaries by - * overriding entryRemoved. - */ - private static class DictionaryFacilitatorLruCacheInner extends - LruCache<Locale, DictionaryFacilitator> { - private final HashSet<Locale> mCachedLocales; - public DictionaryFacilitatorLruCacheInner(final HashSet<Locale> cachedLocales, - final int maxSize) { - super(maxSize); - mCachedLocales = cachedLocales; - } - - @Override - protected void entryRemoved(boolean evicted, Locale key, - DictionaryFacilitator oldValue, DictionaryFacilitator newValue) { - if (oldValue != null && oldValue != newValue) { - oldValue.closeDictionaries(); - } - if (key != null && newValue == null) { - // Remove locale from the cache when the dictionary facilitator for the locale is - // evicted and new facilitator is not set for the locale. - mCachedLocales.remove(key); - if (size() >= maxSize()) { - Log.w(TAG, "DictionaryFacilitator for " + key.toString() - + " has been evicted due to cache size limit." - + " size: " + size() + ", maxSize: " + maxSize()); - } - } - } - } - private final Context mContext; - private final HashSet<Locale> mCachedLocales = new HashSet<>(); private final String mDictionaryNamePrefix; - private final DictionaryFacilitatorLruCacheInner mLruCache; private final Object mLock = new Object(); - private boolean mUseContactsDictionary = false; + private final DictionaryFacilitator mDictionaryFacilitator; + private boolean mUseContactsDictionary; + private Locale mLocale; public DictionaryFacilitatorLruCache(final Context context, final String dictionaryNamePrefix) { mContext = context; - mLruCache = new DictionaryFacilitatorLruCacheInner( - mCachedLocales, MAX_DICTIONARY_FACILITATOR_COUNT); mDictionaryNamePrefix = dictionaryNamePrefix; + mDictionaryFacilitator = DictionaryFacilitatorProvider.getDictionaryFacilitator(); } private static void waitForLoadingMainDictionary( @@ -101,59 +63,40 @@ public class DictionaryFacilitatorLruCache { } } - private void resetDictionariesForLocaleLocked(final DictionaryFacilitator dictionaryFacilitator, - final Locale locale) { + private void resetDictionariesForLocaleLocked() { // Note: Given that personalized dictionaries are not used here; we can pass null account. - dictionaryFacilitator.resetDictionaries(mContext, new Locale[]{locale}, + mDictionaryFacilitator.resetDictionaries(mContext, new Locale[]{mLocale}, mUseContactsDictionary, false /* usePersonalizedDicts */, false /* forceReloadMainDictionary */, null /* account */, mDictionaryNamePrefix, null /* listener */); } - public void setUseContactsDictionary(final boolean useContectsDictionary) { - if (mUseContactsDictionary == useContectsDictionary) { - // The value has not been changed. - return; - } + public void setUseContactsDictionary(final boolean useContactsDictionary) { synchronized (mLock) { - mUseContactsDictionary = useContectsDictionary; - for (final Locale locale : mCachedLocales) { - final DictionaryFacilitator dictionaryFacilitator = mLruCache.get(locale); - resetDictionariesForLocaleLocked(dictionaryFacilitator, locale); - waitForLoadingMainDictionary(dictionaryFacilitator); + if (mUseContactsDictionary == useContactsDictionary) { + // The value has not been changed. + return; } + mUseContactsDictionary = useContactsDictionary; + resetDictionariesForLocaleLocked(); + waitForLoadingMainDictionary(mDictionaryFacilitator); } } public DictionaryFacilitator get(final Locale locale) { - DictionaryFacilitator dictionaryFacilitator = mLruCache.get(locale); - if (dictionaryFacilitator != null) { - // dictionary facilitator for the locale is in the cache. - return dictionaryFacilitator; - } synchronized (mLock) { - dictionaryFacilitator = mLruCache.get(locale); - if (dictionaryFacilitator != null) { - return dictionaryFacilitator; + if (!mDictionaryFacilitator.isForLocales(new Locale[]{locale})) { + mLocale = locale; + resetDictionariesForLocaleLocked(); } - dictionaryFacilitator = DictionaryFacilitatorProvider.newDictionaryFacilitator(); - resetDictionariesForLocaleLocked(dictionaryFacilitator, locale); - waitForLoadingMainDictionary(dictionaryFacilitator); - mLruCache.put(locale, dictionaryFacilitator); - mCachedLocales.add(locale); - return dictionaryFacilitator; + waitForLoadingMainDictionary(mDictionaryFacilitator); + return mDictionaryFacilitator; } } - public void evictAll() { + public void closeDictionaries() { synchronized (mLock) { - mLruCache.evictAll(); - mCachedLocales.clear(); + mDictionaryFacilitator.closeDictionaries(); } } - - @UsedForTesting - HashSet<Locale> getCachedLocalesForTesting() { - return mCachedLocales; - } } diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java index 8c780027b..064d79b3c 100644 --- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java @@ -164,12 +164,11 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { } private void asyncExecuteTaskWithWriteLock(final Runnable task) { - asyncExecuteTaskWithLock(mLock.writeLock(), mDictName /* executorName */, task); + asyncExecuteTaskWithLock(mLock.writeLock(), task); } - private static void asyncExecuteTaskWithLock(final Lock lock, final String executorName, - final Runnable task) { - ExecutorUtils.getExecutor(executorName).execute(new Runnable() { + private static void asyncExecuteTaskWithLock(final Lock lock, final Runnable task) { + ExecutorUtils.getExecutorForDynamicLanguageModelUpdate().execute(new Runnable() { @Override public void run() { lock.lock(); @@ -663,7 +662,7 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { final String dictName = mDictName; final File dictFile = mDictFile; final AsyncResultHolder<DictionaryStats> result = new AsyncResultHolder<>(); - asyncExecuteTaskWithLock(mLock.readLock(), dictName /* executorName */, new Runnable() { + asyncExecuteTaskWithLock(mLock.readLock(), new Runnable() { @Override public void run() { final BinaryDictionary binaryDictionary = getBinaryDictionary(); @@ -714,7 +713,7 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { reloadDictionaryIfRequired(); final String tag = TAG; final String dictName = mDictName; - asyncExecuteTaskWithLock(mLock.readLock(), "dumpAllWordsForDebug", new Runnable() { + asyncExecuteTaskWithLock(mLock.readLock(), new Runnable() { @Override public void run() { Log.d(tag, "Dump dictionary: " + dictName + " for " + mLocale); @@ -752,7 +751,7 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { public WordProperty[] getWordPropertiesForSyncing() { reloadDictionaryIfRequired(); final AsyncResultHolder<WordProperty[]> result = new AsyncResultHolder<>(); - asyncExecuteTaskWithLock(mLock.readLock(), "sync-read", new Runnable() { + asyncExecuteTaskWithLock(mLock.readLock(), new Runnable() { @Override public void run() { final ArrayList<WordProperty> wordPropertyList = new ArrayList<>(); diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index e7917ab90..550efa59f 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -127,7 +127,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen final Settings mSettings; private final DictionaryFacilitator mDictionaryFacilitator = - DictionaryFacilitatorProvider.newDictionaryFacilitator(); + DictionaryFacilitatorProvider.getDictionaryFacilitator(); final InputLogic mInputLogic = new InputLogic(this /* LatinIME */, this /* SuggestionStripViewAccessor */, mDictionaryFacilitator); // We expect to have only one decoder in almost all cases, hence the default capacity of 1. diff --git a/java/src/com/android/inputmethod/latin/SystemBroadcastReceiver.java b/java/src/com/android/inputmethod/latin/SystemBroadcastReceiver.java index db5e632ae..5c3abd2db 100644 --- a/java/src/com/android/inputmethod/latin/SystemBroadcastReceiver.java +++ b/java/src/com/android/inputmethod/latin/SystemBroadcastReceiver.java @@ -20,6 +20,7 @@ import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.os.Process; import android.util.Log; @@ -35,6 +36,22 @@ import com.android.inputmethod.latin.utils.UncachedInputMethodManagerUtils; * package has been replaced by a newer version of the same package. This class also detects * {@link Intent#ACTION_BOOT_COMPLETED} and {@link Intent#ACTION_USER_INITIALIZE} broadcast intent. * + * If this IME has already been installed in the system image and a new version of this IME has + * been installed, {@link Intent#ACTION_MY_PACKAGE_REPLACED} is received by this receiver and it + * will hide the setup wizard's icon. + * + * If this IME has already been installed in the data partition and a new version of this IME has + * been installed, {@link Intent#ACTION_MY_PACKAGE_REPLACED} is received by this receiver but it + * will not hide the setup wizard's icon, and the icon will appear on the launcher. + * + * If this IME hasn't been installed yet and has been newly installed, no + * {@link Intent#ACTION_MY_PACKAGE_REPLACED} will be sent and the setup wizard's icon will appear + * on the launcher. + * + * When the device has been booted, {@link Intent#ACTION_BOOT_COMPLETED} is received by this + * receiver and it checks whether the setup wizard's icon should be appeared or not on the launcher + * depending on which partition this IME is installed. + * * When the system locale has been changed, {@link Intent#ACTION_LOCALE_CHANGED} is received by * this receiver and the {@link KeyboardLayoutSet}'s cache is cleared. */ @@ -52,21 +69,22 @@ public final class SystemBroadcastReceiver extends BroadcastReceiver { final RichInputMethodManager richImm = RichInputMethodManager.getInstance(); final InputMethodSubtype[] additionalSubtypes = richImm.getAdditionalSubtypes(); richImm.setAdditionalInputMethodSubtypes(additionalSubtypes); - showAppIcon(context); + toggleAppIcon(context); } else if (Intent.ACTION_BOOT_COMPLETED.equals(intentAction)) { Log.i(TAG, "Boot has been completed"); - showAppIcon(context); + toggleAppIcon(context); } else if (Intent.ACTION_LOCALE_CHANGED.equals(intentAction)) { Log.i(TAG, "System locale changed"); KeyboardLayoutSet.onSystemLocaleChanged(); } // The process that hosts this broadcast receiver is invoked and remains alive even after - // 1) the package has been re-installed, 2) the device has just booted, + // 1) the package has been re-installed, + // 2) the device has just booted, // 3) a new user has been created. // There is no good reason to keep the process alive if this IME isn't a current IME. - final InputMethodManager imm = - (InputMethodManager)context.getSystemService(Context.INPUT_METHOD_SERVICE); + final InputMethodManager imm = (InputMethodManager) + context.getSystemService(Context.INPUT_METHOD_SERVICE); // Called to check whether this IME has been triggered by the current user or not final boolean isInputMethodManagerValidForUserOfThisProcess = !imm.getInputMethodList().isEmpty(); @@ -79,12 +97,17 @@ public final class SystemBroadcastReceiver extends BroadcastReceiver { } } - private static void showAppIcon(final Context context) { - final ComponentName setupWizardActivity = new ComponentName(context, SetupActivity.class); - final PackageManager pm = context.getPackageManager(); - pm.setComponentEnabledSetting( - setupWizardActivity, - PackageManager.COMPONENT_ENABLED_STATE_ENABLED, + private 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)) { + Log.i(TAG, "toggleAppIcon() : FLAG_SYSTEM = " + isSystemApp); + } + context.getPackageManager().setComponentEnabledSetting( + new ComponentName(context, SetupActivity.class), + isSystemApp + ? PackageManager.COMPONENT_ENABLED_STATE_DISABLED + : PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP); } } diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java index 9154cc35a..56be23f5b 100644 --- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java +++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java @@ -1007,7 +1007,8 @@ public final class InputLogic { mWordComposer.reset(); mWordComposer.setRejectedBatchModeSuggestion(rejectedSuggestion); if (!TextUtils.isEmpty(rejectedSuggestion)) { - mDictionaryFacilitator.removeWordFromPersonalizedDicts(rejectedSuggestion); + unlearnWord(rejectedSuggestion, inputTransaction.mSettingsValues, + Constants.EVENT_REJECTION); } StatsUtils.onBackspaceWordDelete(rejectedSuggestion.length()); } else { @@ -1060,6 +1061,8 @@ public final class InputLogic { } } + boolean hasUnlearnedWordBeingDeleted = false; + // No cancelling of commit/double space/swap: we have a regular backspace. // We should backspace one char and restart suggestion if at the end of a word. if (mConnection.hasSelection()) { @@ -1090,6 +1093,11 @@ public final class InputLogic { sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL); int totalDeletedLength = 1; if (mDeleteCount > Constants.DELETE_ACCELERATE_AT) { + // If this is an accelerated (i.e., double) deletion, then we need to + // consider unlearning here too because we may have just entered the + // previous word, and the next deletion will currupt it. + hasUnlearnedWordBeingDeleted |= unlearnWordBeingDeleted( + inputTransaction.mSettingsValues, currentKeyboardScriptId); sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL); totalDeletedLength++; } @@ -1112,6 +1120,11 @@ public final class InputLogic { mConnection.deleteSurroundingText(lengthToDelete, 0); int totalDeletedLength = lengthToDelete; if (mDeleteCount > Constants.DELETE_ACCELERATE_AT) { + // If this is an accelerated (i.e., double) deletion, then we need to + // consider unlearning here too because we may have just entered the + // previous word, and the next deletion will currupt it. + hasUnlearnedWordBeingDeleted |= unlearnWordBeingDeleted( + inputTransaction.mSettingsValues, currentKeyboardScriptId); final int codePointBeforeCursorToDeleteAgain = mConnection.getCodePointBeforeCursor(); if (codePointBeforeCursorToDeleteAgain != Constants.NOT_A_CODE) { @@ -1124,6 +1137,11 @@ public final class InputLogic { StatsUtils.onBackspacePressed(totalDeletedLength); } } + if (!hasUnlearnedWordBeingDeleted) { + // Consider unlearning the word being deleted (if we have not done so already). + unlearnWordBeingDeleted( + inputTransaction.mSettingsValues, currentKeyboardScriptId); + } if (inputTransaction.mSettingsValues.isSuggestionsEnabledPerUserSettings() && inputTransaction.mSettingsValues.mSpacingAndPunctuations .mCurrentLanguageHasSpaces @@ -1135,6 +1153,38 @@ 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. + if (!mConnection.hasSelection() + && settingsValues.isSuggestionsEnabledPerUserSettings() + && settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces + && !mConnection.isCursorFollowedByWordCharacter( + settingsValues.mSpacingAndPunctuations)) { + final TextRange range = mConnection.getWordRangeAtCursor( + settingsValues.mSpacingAndPunctuations, + currentKeyboardScriptId); + final String wordBeingDeleted = range.mWord.toString(); + if (!wordBeingDeleted.isEmpty()) { + unlearnWord(wordBeingDeleted, settingsValues, + Constants.EVENT_BACKSPACE); + return true; + } + } + return false; + } + + void unlearnWord(final String word, final SettingsValues settingsValues, final int eventType) { + final NgramContext ngramContext = mConnection.getNgramContextFromNthPreviousWord( + settingsValues.mSpacingAndPunctuations, 2); + final int timeStampInSeconds = (int)TimeUnit.MILLISECONDS.toSeconds( + System.currentTimeMillis()); + mDictionaryFacilitator.unlearnFromUserHistory( + word, ngramContext, timeStampInSeconds, eventType); + } + /** * Handle a press on the language switch key (the "globe key") */ @@ -1546,7 +1596,8 @@ public final class InputLogic { } mConnection.deleteSurroundingText(deleteLength, 0); if (!TextUtils.isEmpty(committedWord)) { - mDictionaryFacilitator.removeWordFromPersonalizedDicts(committedWordString); + unlearnWord(committedWordString, inputTransaction.mSettingsValues, + Constants.EVENT_REVERT); } final String stringToCommit = originallyTypedWord + (usePhantomSpace ? "" : separatorString); diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java index 95293bf2f..d35d1f2f5 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java @@ -140,8 +140,8 @@ public final class AndroidSpellCheckerService extends SpellCheckerService @Override public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) { if (!PREF_USE_CONTACTS_KEY.equals(key)) return; - final boolean useContactsDictionary = prefs.getBoolean(PREF_USE_CONTACTS_KEY, true); - mDictionaryFacilitatorCache.setUseContactsDictionary(useContactsDictionary); + final boolean useContactsDictionary = prefs.getBoolean(PREF_USE_CONTACTS_KEY, true); + mDictionaryFacilitatorCache.setUseContactsDictionary(useContactsDictionary); } @Override @@ -226,7 +226,7 @@ public final class AndroidSpellCheckerService extends SpellCheckerService public boolean onUnbind(final Intent intent) { mSemaphore.acquireUninterruptibly(MAX_NUM_OF_THREADS_READ_DICTIONARY); try { - mDictionaryFacilitatorCache.evictAll(); + mDictionaryFacilitatorCache.closeDictionaries(); } finally { mSemaphore.release(MAX_NUM_OF_THREADS_READ_DICTIONARY); } diff --git a/java/src/com/android/inputmethod/latin/spellcheck/UserDictionaryLookup.java b/java/src/com/android/inputmethod/latin/spellcheck/UserDictionaryLookup.java index baff8f066..856f16a53 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/UserDictionaryLookup.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/UserDictionaryLookup.java @@ -26,14 +26,13 @@ import android.util.Log; import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.latin.common.LocaleUtils; +import com.android.inputmethod.latin.utils.ExecutorUtils; import java.io.Closeable; import java.util.ArrayList; import java.util.HashMap; import java.util.Locale; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -83,12 +82,6 @@ public class UserDictionaryLookup implements Closeable { private final ContentResolver mResolver; /** - * Executor on which to perform the initial load and subsequent reloads (after a delay). - */ - private final ScheduledExecutorService mLoadExecutor = - Executors.newSingleThreadScheduledExecutor(); - - /** * Runnable that calls loadUserDictionary(). */ private class UserDictionaryLoader implements Runnable { @@ -150,7 +143,8 @@ public class UserDictionaryLookup implements Closeable { } // Schedule a new reload after RELOAD_DELAY_MS. - mReloadFuture = mLoadExecutor.schedule(mLoader, RELOAD_DELAY_MS, TimeUnit.MILLISECONDS); + mReloadFuture = ExecutorUtils.getExecutorForDynamicLanguageModelUpdate().schedule( + mLoader, RELOAD_DELAY_MS, TimeUnit.MILLISECONDS); } } private final ContentObserver mObserver = new UserDictionaryContentObserver(); @@ -192,7 +186,7 @@ public class UserDictionaryLookup implements Closeable { // 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. - mLoadExecutor.schedule(mLoader, 0, TimeUnit.MILLISECONDS); + ExecutorUtils.getExecutorForDynamicLanguageModelUpdate().execute(mLoader); // Register the observer to be notified on changes to the UserDictionary and all individual // items. @@ -236,9 +230,6 @@ public class UserDictionaryLookup implements Closeable { Log.d(TAG, "Close called (no pun intended), cleaning up executor and observer"); } if (mIsClosed.compareAndSet(false, true)) { - // Shut down the load executor. - mLoadExecutor.shutdown(); - // Unregister the content observer. mResolver.unregisterContentObserver(mObserver); } @@ -342,8 +333,7 @@ public class UserDictionaryLookup implements Closeable { if (DEBUG) { Log.d(TAG, "Loading UserDictionary"); } - HashMap<String, ArrayList<Locale>> dictWords = - new HashMap<String, ArrayList<Locale>>(); + HashMap<String, ArrayList<Locale>> dictWords = 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, @@ -413,7 +403,7 @@ public class UserDictionaryLookup implements Closeable { Log.d(TAG, "Word [" + dictWord + "] not seen for other locales, creating new entry"); } - dictLocales = new ArrayList<Locale>(); + dictLocales = new ArrayList<>(); dictWords.put(dictWord, dictLocales); } // Append the locale to the list of locales this word is in. diff --git a/java/src/com/android/inputmethod/latin/utils/ExecutorUtils.java b/java/src/com/android/inputmethod/latin/utils/ExecutorUtils.java index 50be16072..c533a6273 100644 --- a/java/src/com/android/inputmethod/latin/utils/ExecutorUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/ExecutorUtils.java @@ -16,10 +16,12 @@ package com.android.inputmethod.latin.utils; +import android.util.Log; + import com.android.inputmethod.annotations.UsedForTesting; +import java.lang.Thread.UncaughtExceptionHandler; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ThreadFactory; @@ -28,33 +30,49 @@ import java.util.concurrent.ThreadFactory; * Utilities to manage executors. */ public class ExecutorUtils { - static final ConcurrentHashMap<String, ScheduledExecutorService> sExecutorMap = + + private static final String STATIC_LANGUAGE_MODEL_UPDATE = "StaticLanguageModelUpdate"; + private static final String DYNAMIC_LANGUAGE_MODEL_UPDATE = "DynamicLanguageModelUpdate"; + + private static final ConcurrentHashMap<String, ScheduledExecutorService> sExecutorMap = new ConcurrentHashMap<>(); - private static class ThreadFactoryWithId implements ThreadFactory { - private final String mId; + @UsedForTesting + private static ScheduledExecutorService sExecutorServiceForTests; - public ThreadFactoryWithId(final String id) { - mId = id; - } + @UsedForTesting + public static void setExecutorServiceForTests( + final ScheduledExecutorService executorServiceForTests) { + sExecutorServiceForTests = executorServiceForTests; + } - @Override - public Thread newThread(final Runnable r) { - return new Thread(r, "Executor - " + mId); - } + /** + * @return scheduled executor service used to update static language models + */ + public static ScheduledExecutorService getExecutorForStaticLanguageModelUpdate() { + return getExecutor(STATIC_LANGUAGE_MODEL_UPDATE); + } + + /** + * @return scheduled executor service used to update dynamic language models + */ + public static ScheduledExecutorService getExecutorForDynamicLanguageModelUpdate() { + return getExecutor(DYNAMIC_LANGUAGE_MODEL_UPDATE); } /** * Gets the executor for the given id. */ - public static ScheduledExecutorService getExecutor(final String id) { + private static ScheduledExecutorService getExecutor(final String id) { + if (sExecutorServiceForTests != null) { + return sExecutorServiceForTests; + } ScheduledExecutorService executor = sExecutorMap.get(id); if (executor == null) { synchronized (sExecutorMap) { executor = sExecutorMap.get(id); if (executor == null) { - executor = Executors.newSingleThreadScheduledExecutor( - new ThreadFactoryWithId(id)); + executor = Executors.newSingleThreadScheduledExecutor(new ExecutorFactory(id)); sExecutorMap.put(id, executor); } } @@ -69,14 +87,42 @@ public class ExecutorUtils { public static void shutdownAllExecutors() { synchronized (sExecutorMap) { for (final ScheduledExecutorService executor : sExecutorMap.values()) { - executor.execute(new Runnable() { - @Override - public void run() { - executor.shutdown(); - sExecutorMap.remove(executor); - } - }); + executor.execute(new ExecutorShutdown(executor)); } + sExecutorMap.clear(); + } + } + + private static class ExecutorFactory implements ThreadFactory { + private final String mThreadName; + + public ExecutorFactory(final String threadName) { + mThreadName = threadName; + } + + @Override + public Thread newThread(final Runnable runnable) { + Thread thread = new Thread(runnable, mThreadName); + thread.setUncaughtExceptionHandler(new UncaughtExceptionHandler() { + @Override + public void uncaughtException(Thread thread, Throwable ex) { + Log.w(mThreadName + "-" + runnable.getClass().getSimpleName(), ex); + } + }); + return thread; + } + } + + private static class ExecutorShutdown implements Runnable { + private final ScheduledExecutorService mExecutor; + + public ExecutorShutdown(final ScheduledExecutorService executor) { + mExecutor = executor; + } + + @Override + public void run() { + mExecutor.shutdown(); } } } |