Merge "Add StatsUtils method for handling subtype change."
diff --git a/common/src/com/android/inputmethod/latin/common/Constants.java b/common/src/com/android/inputmethod/latin/common/Constants.java
index 03abb0f..b491c8c 100644
--- a/common/src/com/android/inputmethod/latin/common/Constants.java
+++ b/common/src/com/android/inputmethod/latin/common/Constants.java
@@ -325,6 +325,10 @@
     public static final int DECODER_SCORE_SCALAR = 1000000;
     public static final int DECODER_MAX_SCORE = 1000000000;
 
+    public static final int EVENT_BACKSPACE = 1;
+    public static final int EVENT_REJECTION = 2;
+    public static final int EVENT_REVERT = 3;
+
     private Constants() {
         // This utility class is not publicly instantiable.
     }
diff --git a/java-overridable/src/com/android/inputmethod/latin/DictionaryFacilitatorProvider.java b/java-overridable/src/com/android/inputmethod/latin/DictionaryFacilitatorProvider.java
index 205d648..2a3774b 100644
--- a/java-overridable/src/com/android/inputmethod/latin/DictionaryFacilitatorProvider.java
+++ b/java-overridable/src/com/android/inputmethod/latin/DictionaryFacilitatorProvider.java
@@ -20,7 +20,7 @@
  * Factory for instantiating DictionaryFacilitator objects.
  */
 public class DictionaryFacilitatorProvider {
-    public static DictionaryFacilitator newDictionaryFacilitator() {
+    public static DictionaryFacilitator getDictionaryFacilitator() {
         return new DictionaryFacilitatorImpl();
     }
 }
diff --git a/java/src/com/android/inputmethod/latin/ContactsContentObserver.java b/java/src/com/android/inputmethod/latin/ContactsContentObserver.java
index 019d17d..e45681b 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.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 @@
         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 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 dc5abd9..1fadc6f 100644
--- a/java/src/com/android/inputmethod/latin/ContactsManager.java
+++ b/java/src/com/android/inputmethod/latin/ContactsManager.java
@@ -34,7 +34,6 @@
  * 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 c22dc28..addc8f2 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java
@@ -150,7 +150,9 @@
             @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 @@
     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 3d76751..1e08854 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 @@
             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 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 @@
         }
     }
 
+    @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 @@
         }
     }
 
+    @Override
     public ArrayList<Pair<String, DictionaryStats>> getStatsOfEnabledSubDicts() {
         final ArrayList<Pair<String, DictionaryStats>> statsOfEnabledSubDicts = new ArrayList<>();
         final DictionaryGroup[] dictionaryGroups = mDictionaryGroups;
@@ -795,18 +804,4 @@
         }
         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 85ecf93..b813af4 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 @@
  */
 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 @@
         }
     }
 
-    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 8c78002..064d79b 100644
--- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
@@ -164,12 +164,11 @@
     }
 
     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 @@
         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 @@
         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 @@
     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 e7917ab..550efa5 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -127,7 +127,7 @@
 
     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 db5e632..5c3abd2 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.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 @@
  * 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 @@
             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 @@
         }
     }
 
-    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 9154cc3..56be23f 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 @@
                 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 @@
                 }
             }
 
+            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 @@
                     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 @@
                     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 @@
                     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 @@
         }
     }
 
+    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 @@
         }
         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 95293bf..d35d1f2 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 @@
     @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 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 baff8f0..856f16a 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 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 @@
     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 @@
             }
 
             // 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 @@
         // 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 @@
             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 @@
         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 @@
                         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 50be160..c533a62 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 @@
  * 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 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();
         }
     }
 }
diff --git a/tests/src/com/android/inputmethod/latin/ContactsContentObserverTest.java b/tests/src/com/android/inputmethod/latin/ContactsContentObserverTest.java
new file mode 100644
index 0000000..f90a18b
--- /dev/null
+++ b/tests/src/com/android/inputmethod/latin/ContactsContentObserverTest.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.latin;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.validateMockitoUsage;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.provider.ContactsContract.Contacts;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+
+/**
+ * Tests for {@link ContactsContentObserver}.
+ */
+@SmallTest
+public class ContactsContentObserverTest {
+    private static final int UPDATED_CONTACT_COUNT = 10;
+    private static final int STALE_CONTACT_COUNT = 8;
+    private static final ArrayList<String> STALE_NAMES_LIST = new ArrayList<>();
+    private static final ArrayList<String> UPDATED_NAMES_LIST = new ArrayList<>();
+
+    static {
+        STALE_NAMES_LIST.add("Larry Page");
+        STALE_NAMES_LIST.add("Roger Federer");
+        UPDATED_NAMES_LIST.add("Larry Page");
+        UPDATED_NAMES_LIST.add("Roger Federer");
+        UPDATED_NAMES_LIST.add("Barak Obama");
+    }
+
+    @Mock private ContactsManager mMockManager;
+    @Mock private Context mContext;
+
+    private ContactsContentObserver mObserver;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mObserver = new ContactsContentObserver(mMockManager, mContext);
+    }
+
+    @After
+    public void tearDown() {
+        validateMockitoUsage();
+    }
+
+    @Test
+    public void testHaveContentsChanged_NoChange() {
+        when(mMockManager.getContactCount()).thenReturn(STALE_CONTACT_COUNT);
+        when(mMockManager.getContactCountAtLastRebuild()).thenReturn(STALE_CONTACT_COUNT);
+        when(mMockManager.getValidNames(eq(Contacts.CONTENT_URI))).thenReturn(STALE_NAMES_LIST);
+        when(mMockManager.getHashCodeAtLastRebuild()).thenReturn(STALE_NAMES_LIST.hashCode());
+        assertFalse(mObserver.haveContentsChanged());
+    }
+    @Test
+    public void testHaveContentsChanged_UpdatedCount() {
+        when(mMockManager.getContactCount()).thenReturn(UPDATED_CONTACT_COUNT);
+        when(mMockManager.getContactCountAtLastRebuild()).thenReturn(STALE_CONTACT_COUNT);
+        assertTrue(mObserver.haveContentsChanged());
+    }
+
+    @Test
+    public void testHaveContentsChanged_HashUpdate() {
+        when(mMockManager.getContactCount()).thenReturn(STALE_CONTACT_COUNT);
+        when(mMockManager.getContactCountAtLastRebuild()).thenReturn(STALE_CONTACT_COUNT);
+        when(mMockManager.getValidNames(eq(Contacts.CONTENT_URI))).thenReturn(UPDATED_NAMES_LIST);
+        when(mMockManager.getHashCodeAtLastRebuild()).thenReturn(STALE_NAMES_LIST.hashCode());
+        assertTrue(mObserver.haveContentsChanged());
+    }
+}
diff --git a/tests/src/com/android/inputmethod/latin/ContactsDictionaryUtilsTest.java b/tests/src/com/android/inputmethod/latin/ContactsDictionaryUtilsTest.java
new file mode 100644
index 0000000..9b49f1a
--- /dev/null
+++ b/tests/src/com/android/inputmethod/latin/ContactsDictionaryUtilsTest.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.latin;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.test.suitebuilder.annotation.SmallTest;
+
+import org.junit.Test;
+
+import java.util.Locale;
+
+/**
+ * Tests for {@link ContactsDictionaryUtils}
+ */
+@SmallTest
+public class ContactsDictionaryUtilsTest {
+
+    @Test
+    public void testGetWordEndPosition() {
+        final String testString1 = "Larry Page";
+        assertEquals(5, ContactsDictionaryUtils.getWordEndPosition(
+                testString1, testString1.length(), 0 /* startIndex */));
+
+        assertEquals(10, ContactsDictionaryUtils.getWordEndPosition(
+                testString1, testString1.length(), 6 /* startIndex */));
+
+        final String testString2 = "Larry-Page";
+        assertEquals(10, ContactsDictionaryUtils.getWordEndPosition(
+                testString2, testString1.length(), 0 /* startIndex */));
+
+        final String testString3 = "Larry'Page";
+        assertEquals(10, ContactsDictionaryUtils.getWordEndPosition(
+                testString3, testString1.length(), 0 /* startIndex */));
+    }
+
+    @Test
+    public void testUseFirstLastBigramsForLocale() {
+        assertTrue(ContactsDictionaryUtils.useFirstLastBigramsForLocale(Locale.ENGLISH));
+        assertTrue(ContactsDictionaryUtils.useFirstLastBigramsForLocale(Locale.US));
+        assertTrue(ContactsDictionaryUtils.useFirstLastBigramsForLocale(Locale.UK));
+        assertFalse(ContactsDictionaryUtils.useFirstLastBigramsForLocale(Locale.CHINA));
+        assertFalse(ContactsDictionaryUtils.useFirstLastBigramsForLocale(Locale.GERMAN));
+    }
+}
diff --git a/tests/src/com/android/inputmethod/latin/ContactsManagerTest.java b/tests/src/com/android/inputmethod/latin/ContactsManagerTest.java
new file mode 100644
index 0000000..6326b3b
--- /dev/null
+++ b/tests/src/com/android/inputmethod/latin/ContactsManagerTest.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.latin;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts;
+import android.test.AndroidTestCase;
+import android.test.RenamingDelegatingContext;
+import android.test.mock.MockContentProvider;
+import android.test.mock.MockContentResolver;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/**
+ * Tests for {@link ContactsManager}
+ */
+@SmallTest
+public class ContactsManagerTest extends AndroidTestCase {
+
+    private ContactsManager mManager;
+    private FakeContactsContentProvider mFakeContactsContentProvider;
+    private MatrixCursor mMatrixCursor;
+
+    @Before
+    @Override
+    public void setUp() throws Exception {
+        // Fake content provider
+        mFakeContactsContentProvider = new FakeContactsContentProvider();
+        mMatrixCursor = new MatrixCursor(ContactsDictionaryConstants.PROJECTION);
+        // Add the fake content provider to fake content resolver.
+        final MockContentResolver contentResolver = new MockContentResolver();
+        contentResolver.addProvider(ContactsContract.AUTHORITY, mFakeContactsContentProvider);
+        // Add the fake content resolver to a fake context.
+        final ContextWithMockContentResolver context = new ContextWithMockContentResolver(mContext);
+        context.setContentResolver(contentResolver);
+
+        mManager = new ContactsManager(context);
+    }
+
+    @Test
+    public void testGetValidNames() {
+        final String contactName1 = "firstname lastname";
+        final String contactName2 = "larry";
+        mMatrixCursor.addRow(new Object[] { 1, contactName1 });
+        mMatrixCursor.addRow(new Object[] { 2, null /* null name */ });
+        mMatrixCursor.addRow(new Object[] { 3, contactName2 });
+        mMatrixCursor.addRow(new Object[] { 4, "floopy@example.com" /* invalid name */ });
+        mFakeContactsContentProvider.addQueryResult(Contacts.CONTENT_URI, mMatrixCursor);
+
+        final ArrayList<String> validNames = mManager.getValidNames(Contacts.CONTENT_URI);
+        assertEquals(2, validNames.size());
+        assertEquals(contactName1, validNames.get(0));
+        assertEquals(contactName2, validNames.get(1));
+    }
+
+    @Test
+    public void testGetCount() {
+        mMatrixCursor.addRow(new Object[] { 1, "firstname" });
+        mMatrixCursor.addRow(new Object[] { 2, null /* null name */ });
+        mMatrixCursor.addRow(new Object[] { 3, "larry" });
+        mMatrixCursor.addRow(new Object[] { 4, "floopy@example.com" /* invalid name */ });
+        mFakeContactsContentProvider.addQueryResult(Contacts.CONTENT_URI, mMatrixCursor);
+
+        assertEquals(4, mManager.getContactCount());
+    }
+
+
+    static class ContextWithMockContentResolver extends RenamingDelegatingContext {
+        private ContentResolver contentResolver;
+
+        public void setContentResolver(final ContentResolver contentResolver) {
+            this.contentResolver = contentResolver;
+        }
+
+        public ContextWithMockContentResolver(final Context targetContext) {
+            super(targetContext, "test");
+        }
+
+        @Override
+        public ContentResolver getContentResolver() {
+            return contentResolver;
+        }
+    }
+
+    static class FakeContactsContentProvider extends MockContentProvider {
+        private final HashMap<String, MatrixCursor> mQueryCursorMapForTestExpectations =
+                new HashMap<>();
+
+        @Override
+        public Cursor query(final Uri uri, final String[] projection, final String selection,
+                final String[] selectionArgs, final String sortOrder) {
+            return mQueryCursorMapForTestExpectations.get(uri.toString());
+        }
+
+        public void reset() {
+            mQueryCursorMapForTestExpectations.clear();
+        }
+
+        public void addQueryResult(final Uri uri, final MatrixCursor cursor) {
+            mQueryCursorMapForTestExpectations.put(uri.toString(), cursor);
+        }
+    }
+}
diff --git a/tests/src/com/android/inputmethod/latin/DictionaryFacilitatorLruCacheTests.java b/tests/src/com/android/inputmethod/latin/DictionaryFacilitatorLruCacheTests.java
index 5340b6f..4f924ab 100644
--- a/tests/src/com/android/inputmethod/latin/DictionaryFacilitatorLruCacheTests.java
+++ b/tests/src/com/android/inputmethod/latin/DictionaryFacilitatorLruCacheTests.java
@@ -23,21 +23,6 @@
 
 @LargeTest
 public class DictionaryFacilitatorLruCacheTests extends AndroidTestCase {
-    public void testCacheSize() {
-        final DictionaryFacilitatorLruCache cache =
-                new DictionaryFacilitatorLruCache(getContext(), "");
-
-        assertEquals(0, cache.getCachedLocalesForTesting().size());
-        assertNotNull(cache.get(Locale.US));
-        assertEquals(1, cache.getCachedLocalesForTesting().size());
-        assertNotNull(cache.get(Locale.UK));
-        assertEquals(2, cache.getCachedLocalesForTesting().size());
-        assertNotNull(cache.get(Locale.FRENCH));
-        assertEquals(2, cache.getCachedLocalesForTesting().size());
-        cache.evictAll();
-        assertEquals(0, cache.getCachedLocalesForTesting().size());
-    }
-
     public void testGetFacilitator() {
         final DictionaryFacilitatorLruCache cache =
                 new DictionaryFacilitatorLruCache(getContext(), "");
diff --git a/tests/src/com/android/inputmethod/latin/utils/ExecutorUtilsTests.java b/tests/src/com/android/inputmethod/latin/utils/ExecutorUtilsTests.java
index ae2623d..3b1e43e 100644
--- a/tests/src/com/android/inputmethod/latin/utils/ExecutorUtilsTests.java
+++ b/tests/src/com/android/inputmethod/latin/utils/ExecutorUtilsTests.java
@@ -25,18 +25,17 @@
 import java.util.concurrent.atomic.AtomicInteger;
 
 /**
- * Unit tests for ExecutorUtils.
+ * Unit tests for {@link ExecutorUtils}.
  */
 @MediumTest
 public class ExecutorUtilsTests extends AndroidTestCase {
     private static final String TAG = ExecutorUtilsTests.class.getSimpleName();
 
-    private static final String TEST_EXECUTOR_ID = "test";
     private static final int NUM_OF_TASKS = 10;
     private static final int DELAY_FOR_WAITING_TASKS_MILLISECONDS = 500;
 
     public void testExecute() {
-        final ExecutorService executor = ExecutorUtils.getExecutor(TEST_EXECUTOR_ID);
+        final ExecutorService executor = ExecutorUtils.getExecutorForDynamicLanguageModelUpdate();
         final AtomicInteger v = new AtomicInteger(0);
         for (int i = 0; i < NUM_OF_TASKS; ++i) {
             executor.execute(new Runnable() {