diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
index 02151522d..95293bf2f 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
@@ -24,6 +24,7 @@ import android.text.InputType;
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.InputMethodSubtype;
 import android.view.textservice.SuggestionsInfo;
+import android.util.Log;
 
 import com.android.inputmethod.keyboard.Keyboard;
 import com.android.inputmethod.keyboard.KeyboardId;
@@ -52,6 +53,9 @@ import java.util.concurrent.Semaphore;
  */
 public final class AndroidSpellCheckerService extends SpellCheckerService
         implements SharedPreferences.OnSharedPreferenceChangeListener {
+    private static final String TAG = AndroidSpellCheckerService.class.getSimpleName();
+    private static final boolean DEBUG = false;
+
     public static final String PREF_USE_CONTACTS_KEY = "pref_spellcheck_use_contacts";
 
     private static final int SPELLCHECKER_DUMMY_KEYBOARD_WIDTH = 480;
@@ -80,6 +84,7 @@ public final class AndroidSpellCheckerService extends SpellCheckerService
 
     public static final String SINGLE_QUOTE = "\u0027";
     public static final String APOSTROPHE = "\u2019";
+    private UserDictionaryLookup mUserDictionaryLookup;
 
     public AndroidSpellCheckerService() {
         super();
@@ -95,6 +100,24 @@ public final class AndroidSpellCheckerService extends SpellCheckerService
         final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
         prefs.registerOnSharedPreferenceChangeListener(this);
         onSharedPreferenceChanged(prefs, PREF_USE_CONTACTS_KEY);
+        // Create a UserDictionaryLookup.  It needs to be close()d and set to null in onDestroy.
+        if (mUserDictionaryLookup == null) {
+            if (DEBUG) {
+                Log.d(TAG, "Creating mUserDictionaryLookup in onCreate");
+            }
+            mUserDictionaryLookup = new UserDictionaryLookup(this);
+        } else if (DEBUG) {
+            Log.d(TAG, "mUserDictionaryLookup already created before onCreate");
+        }
+    }
+
+    @Override public void onDestroy() {
+        if (DEBUG) {
+            Log.d(TAG, "Closing and dereferencing mUserDictionaryLookup in onDestroy");
+        }
+        mUserDictionaryLookup.close();
+        mUserDictionaryLookup = null;
+        super.onDestroy();
     }
 
     public float getRecommendedThreshold() {
@@ -150,6 +173,16 @@ public final class AndroidSpellCheckerService extends SpellCheckerService
     public boolean isValidWord(final Locale locale, final String word) {
         mSemaphore.acquireUninterruptibly();
         try {
+            if (mUserDictionaryLookup.isValidWord(word, locale)) {
+                if (DEBUG) {
+                    Log.d(TAG, "mUserDictionaryLookup.isValidWord(" + word + ")=true");
+                }
+                return true;
+            } else {
+                if (DEBUG) {
+                    Log.d(TAG, "mUserDictionaryLookup.isValidWord(" + word + ")=false");
+                }
+            }
             DictionaryFacilitator dictionaryFacilitatorForLocale =
                     mDictionaryFacilitatorCache.get(locale);
             return dictionaryFacilitatorForLocale.isValidSpellingWord(word);
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/UserDictionaryLookup.java b/java/src/com/android/inputmethod/latin/spellcheck/UserDictionaryLookup.java
new file mode 100644
index 000000000..baff8f066
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/spellcheck/UserDictionaryLookup.java
@@ -0,0 +1,430 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.latin.spellcheck;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.UserDictionary;
+import android.util.Log;
+
+import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.latin.common.LocaleUtils;
+
+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;
+
+/**
+ * UserDictionaryLookup provides the ability to lookup into the system-wide "Personal dictionary".
+ *
+ * Note, that the initial dictionary loading happens asynchronously so it is possible (hopefully
+ * rarely) that isValidWord is called before the initial load has started.
+ *
+ * The caller should explicitly call close() when the object is no longer needed, in order to
+ * release any resources and references to this object.  A service should create this object in
+ * onCreate and close() it in onDestroy.
+ */
+public class UserDictionaryLookup implements Closeable {
+    private static final String TAG = UserDictionaryLookup.class.getSimpleName();
+
+    /**
+     * This guards the execution of any Log.d() logging, so that if false, they are not even
+     */
+    private static final boolean DEBUG = false;
+
+    /**
+     * To avoid loading too many dictionary entries in memory, we cap them at this number.  If
+     * that number is exceeded, the lowest-frequency items will be dropped.  Note, there is no
+     * explicit cap on the number of locales in every entry.
+     */
+    private static final int MAX_NUM_ENTRIES = 1000;
+
+    /**
+     * The delay (in milliseconds) to impose on reloads.  Previously scheduled reloads will be
+     * cancelled if a new reload is scheduled before the delay expires.  Thus, only the last
+     * reload in the series of frequent reloads will execute.
+     *
+     * Note, this value should be low enough to allow the "Add to dictionary" feature in the
+     * TextView correction (red underline) drop-down menu to work properly in the following case:
+     *
+     *   1. User types OOV (out-of-vocabulary) word.
+     *   2. The OOV is red-underlined.
+     *   3. User selects "Add to dictionary".  The red underline disappears while the OOV is
+     *      in a composing span.
+     *   4. The user taps space.  The red underline should NOT reappear.  If this value is very
+     *      high and the user performs the space tap fast enough, the red underline may reappear.
+     */
+    @UsedForTesting
+    static final int RELOAD_DELAY_MS = 200;
+
+    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 {
+        @Override
+        public void run() {
+            if (DEBUG) {
+                Log.d(TAG, "Executing (re)load");
+            }
+            loadUserDictionary();
+        }
+    }
+    private final UserDictionaryLoader mLoader = new UserDictionaryLoader();
+
+    /**
+     *  Content observer for UserDictionary changes.  It has the following properties:
+     *    1. It spawns off a UserDictionary reload in another thread, after some delay.
+     *    2. It cancels previously scheduled reloads, and only executes the latest.
+     *    3. It may be called multiple times quickly in succession (and is in fact called so
+     *       when UserDictionary is edited through its settings UI, when sometimes multiple
+     *       notifications are sent for the edited entry, but also for the entire UserDictionary).
+     */
+    private class UserDictionaryContentObserver extends ContentObserver {
+        public UserDictionaryContentObserver() {
+            super(null);
+        }
+
+        @Override
+        public boolean deliverSelfNotifications() {
+            return true;
+        }
+
+        // Support pre-API16 platforms.
+        @Override
+        public void onChange(boolean selfChange) {
+            onChange(selfChange, null);
+        }
+
+        @Override
+        public void onChange(boolean selfChange, Uri uri) {
+            if (DEBUG) {
+                Log.d(TAG, "Received content observer onChange notification for URI: " + uri);
+            }
+            // Cancel (but don't interrupt) any pending reloads (except the initial load).
+            if (mReloadFuture != null && !mReloadFuture.isCancelled() &&
+                    !mReloadFuture.isDone()) {
+                // Note, that if already cancelled or done, this will do nothing.
+                boolean isCancelled = mReloadFuture.cancel(false);
+                if (DEBUG) {
+                    if (isCancelled) {
+                        Log.d(TAG, "Successfully canceled previous reload request");
+                    } else {
+                        Log.d(TAG, "Unable to cancel previous reload request");
+                    }
+                }
+            }
+
+            if (DEBUG) {
+                Log.d(TAG, "Scheduling reload in " + RELOAD_DELAY_MS + " ms");
+            }
+
+            // Schedule a new reload after RELOAD_DELAY_MS.
+            mReloadFuture = mLoadExecutor.schedule(mLoader, RELOAD_DELAY_MS, TimeUnit.MILLISECONDS);
+        }
+    }
+    private final ContentObserver mObserver = new UserDictionaryContentObserver();
+
+    /**
+     * Indicates that a load is in progress, so no need for another.
+     */
+    private AtomicBoolean mIsLoading = new AtomicBoolean(false);
+
+    /**
+     * Indicates that this lookup object has been close()d.
+     */
+    private AtomicBoolean mIsClosed = new AtomicBoolean(false);
+
+    /**
+     * We store a map from a dictionary word to the set of locales it belongs
+     * in. We then iterate over the set of locales to find a match using
+     * LocaleUtils.
+     */
+    private volatile HashMap<String, ArrayList<Locale>> mDictWords;
+
+    /**
+     *  The last-scheduled reload future.  Saved in order to cancel a pending reload if a new one
+     * is coming.
+     */
+    private volatile ScheduledFuture<?> mReloadFuture;
+
+    /**
+     * @param context the context from which to obtain content resolver
+     */
+    public UserDictionaryLookup(Context context) {
+        if (DEBUG) {
+            Log.d(TAG, "UserDictionaryLookup constructor with context: " + context);
+        }
+
+        // Obtain a content resolver.
+        mResolver = context.getContentResolver();
+
+        // 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);
+
+        // Register the observer to be notified on changes to the UserDictionary and all individual
+        // items.
+        //
+        // If the user is interacting with the UserDictionary settings UI, or with the
+        // "Add to dictionary" drop-down option, duplicate notifications will be sent for the same
+        // edit: if a new entry is added, there is a notification for the entry itself, and
+        // separately for the entire dictionary. However, when used programmatically,
+        // only notifications for the specific edits are sent. Thus, the observer is registered to
+        // receive every possible notification, and instead has throttling logic to avoid doing too
+        // many reloads.
+        mResolver.registerContentObserver(
+                UserDictionary.Words.CONTENT_URI, true /* notifyForDescendents */, mObserver);
+    }
+
+    /**
+     * To be called by the garbage collector in the off chance that the service did not clean up
+     * properly.  Do not rely on this getting called, and make sure close() is called explicitly.
+     */
+    @Override
+    public void finalize() throws Throwable {
+        try {
+            if (DEBUG) {
+                Log.d(TAG, "Finalize called, calling close()");
+            }
+            close();
+        } finally {
+            super.finalize();
+        }
+    }
+
+    /**
+     * Cleans up UserDictionaryLookup: shuts down any extra threads and unregisters the observer.
+     *
+     * It is safe, but not advised to call this multiple times, and isValidWord would continue to
+     * work, but no data will be reloaded any longer.
+     */
+    @Override
+    public void close() {
+        if (DEBUG) {
+            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);
+        }
+    }
+
+    /**
+     * Returns true if the initial load has been performed.
+     *
+     * @return true if the initial load is successful
+     */
+    @UsedForTesting
+    boolean isLoaded() {
+        return mDictWords != null;
+    }
+
+    /**
+     * Determines if the given word is a valid word in the given locale based on the UserDictionary.
+     * It tries hard to find a match: for example, casing is ignored and if the word is present in a
+     * more general locale (e.g. en or all locales), and isValidWord is asking for a more specific
+     * locale (e.g. en_US), it will be considered a match.
+     *
+     * @param word the word to match
+     * @param locale the locale in which to match the word
+     * @return true iff the word has been matched for this locale in the UserDictionary.
+     */
+    public boolean isValidWord(
+            final String word, final Locale locale) {
+        if (!isLoaded()) {
+            // This is a corner case in the event the initial load of UserDictionary has not
+            // been loaded. In that case, we assume the word is not a valid word in
+            // UserDictionary.
+            if (DEBUG) {
+                Log.d(TAG, "isValidWord invoked, but initial load not complete");
+            }
+            return false;
+        }
+
+        // Atomically obtain the current copy of mDictWords;
+        final HashMap<String, ArrayList<Locale>> dictWords = mDictWords;
+
+        if (DEBUG) {
+            Log.d(TAG, "isValidWord invoked for word [" + word +
+                    "] in locale " + locale);
+        }
+        // Lowercase the word using the given locale. Note, that dictionary
+        // words are lowercased using their locale, and theoretically the
+        // lowercasing between two matching locales may differ. For simplicity
+        // we ignore that possibility.
+        final String lowercased = word.toLowerCase(locale);
+        final ArrayList<Locale> dictLocales = dictWords.get(lowercased);
+        if (null == dictLocales) {
+            if (DEBUG) {
+                Log.d(TAG, "isValidWord=false, since there is no entry for " +
+                        "lowercased word [" + lowercased + "]");
+            }
+            return false;
+        } else {
+            if (DEBUG) {
+                Log.d(TAG, "isValidWord found an entry for lowercased word [" + lowercased +
+                        "]; examining locales");
+            }
+            // Iterate over the locales this word is in.
+            for (final Locale dictLocale : dictLocales) {
+                final int matchLevel = LocaleUtils.getMatchLevel(dictLocale.toString(),
+                        locale.toString());
+                if (DEBUG) {
+                    Log.d(TAG, "matchLevel for dictLocale=" + dictLocale + ", locale=" +
+                            locale + " is " + matchLevel);
+                }
+                if (LocaleUtils.isMatch(matchLevel)) {
+                    if (DEBUG) {
+                        Log.d(TAG, "isValidWord=true, since matchLevel " + matchLevel +
+                                " is a match");
+                    }
+                    return true;
+                }
+                if (DEBUG) {
+                    Log.d(TAG, "matchLevel " + matchLevel + " is not a match");
+                }
+            }
+            if (DEBUG) {
+                Log.d(TAG, "isValidWord=false, since none of the locales matched");
+            }
+            return false;
+        }
+    }
+
+    /**
+     * Loads the UserDictionary in the current thread.
+     *
+     * Only one reload can happen at a time. If already running, will exit quickly.
+     */
+    private void loadUserDictionary() {
+        // Bail out if already in the process of loading.
+        if (!mIsLoading.compareAndSet(false, true)) {
+            if (DEBUG) {
+                Log.d(TAG, "Already in the process of loading UserDictionary, skipping");
+            }
+            return;
+        }
+        if (DEBUG) {
+            Log.d(TAG, "Loading UserDictionary");
+        }
+        HashMap<String, ArrayList<Locale>> dictWords =
+                new HashMap<String, ArrayList<Locale>>();
+        // Load the UserDictionary.  Request that items be returned in the default sort order
+        // for UserDictionary, which is by frequency.
+        Cursor cursor = mResolver.query(UserDictionary.Words.CONTENT_URI,
+                null, null, null, UserDictionary.Words.DEFAULT_SORT_ORDER);
+        if (null == cursor || cursor.getCount() < 1) {
+            if (DEBUG) {
+                Log.d(TAG, "No entries found in UserDictionary");
+            }
+        } else {
+            // Iterate over the entries in the UserDictionary.  Note, that iteration is in
+            // descending frequency by default.
+            while (dictWords.size() < MAX_NUM_ENTRIES && cursor.moveToNext()) {
+                // If there is no column for locale, skip this entry. An empty
+                // locale on the other hand will not be skipped.
+                final int dictLocaleIndex = cursor.getColumnIndex(
+                        UserDictionary.Words.LOCALE);
+                if (dictLocaleIndex < 0) {
+                    if (DEBUG) {
+                        Log.d(TAG, "Encountered UserDictionary entry " +
+                                "without LOCALE, skipping");
+                    }
+                    continue;
+                }
+                // If there is no column for word, skip this entry.
+                final int dictWordIndex = cursor.getColumnIndex(
+                        UserDictionary.Words.WORD);
+                if (dictWordIndex < 0) {
+                    if (DEBUG) {
+                        Log.d(TAG, "Encountered UserDictionary entry without " +
+                                "WORD, skipping");
+                    }
+                    continue;
+                }
+                // If the word is null, skip this entry.
+                final String rawDictWord = cursor.getString(dictWordIndex);
+                if (null == rawDictWord) {
+                    if (DEBUG) {
+                        Log.d(TAG, "Encountered null word");
+                    }
+                    continue;
+                }
+                // If the locale is null, that's interpreted to mean all locales. Note, the special
+                // zz locale for an Alphabet (QWERTY) layout will not match any actual language.
+                String localeString = cursor.getString(dictLocaleIndex);
+                if (null == localeString) {
+                    if (DEBUG) {
+                        Log.d(TAG, "Encountered null locale for word [" +
+                                rawDictWord + "], assuming all locales");
+                    }
+                    // For purposes of LocaleUtils, an empty locale matches
+                    // everything.
+                    localeString = "";
+                }
+                final Locale dictLocale = LocaleUtils.constructLocaleFromString(
+                        localeString);
+                // Lowercase the word before storing it.
+                final String dictWord = rawDictWord.toLowerCase(dictLocale);
+                if (DEBUG) {
+                    Log.d(TAG, "Incorporating UserDictionary word [" + dictWord +
+                            "] for locale " + dictLocale);
+                }
+                // Check if there is an existing entry for this word.
+                ArrayList<Locale> dictLocales = dictWords.get(dictWord);
+                if (null == dictLocales) {
+                    // If there is no entry for this word, create one.
+                    if (DEBUG) {
+                        Log.d(TAG, "Word [" + dictWord +
+                                "] not seen for other locales, creating new entry");
+                    }
+                    dictLocales = new ArrayList<Locale>();
+                    dictWords.put(dictWord, dictLocales);
+                }
+                // Append the locale to the list of locales this word is in.
+                dictLocales.add(dictLocale);
+            }
+        }
+
+        // Atomically replace the copy of mDictWords.
+        mDictWords = dictWords;
+
+        // Allow other calls to loadUserDictionary to execute now.
+        mIsLoading.set(false);
+    }
+}
diff --git a/tests/src/com/android/inputmethod/latin/spellcheck/UserDictionaryLookupTest.java b/tests/src/com/android/inputmethod/latin/spellcheck/UserDictionaryLookupTest.java
new file mode 100644
index 000000000..e5c813942
--- /dev/null
+++ b/tests/src/com/android/inputmethod/latin/spellcheck/UserDictionaryLookupTest.java
@@ -0,0 +1,279 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.latin.spellcheck;
+
+import android.annotation.SuppressLint;
+import android.content.ContentResolver;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.UserDictionary;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.util.Log;
+
+import java.util.HashSet;
+import java.util.Locale;
+
+/**
+ * Unit tests for {@link UserDictionaryLookup}.
+ *
+ * Note, this test doesn't mock out the ContentResolver, in order to make sure UserDictionaryLookup
+ * works in a real setting.
+ */
+@SmallTest
+public class UserDictionaryLookupTest extends AndroidTestCase {
+    private static final String TAG = UserDictionaryLookupTest.class.getSimpleName();
+
+    private ContentResolver mContentResolver;
+    private HashSet<Uri> mAddedBackup;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mContentResolver = mContext.getContentResolver();
+        mAddedBackup = new HashSet<Uri>();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        // Remove all entries added during this test.
+        for (Uri row : mAddedBackup) {
+            mContentResolver.delete(row, null, null);
+        }
+        mAddedBackup.clear();
+
+        super.tearDown();
+    }
+
+    /**
+     * Adds the given word to UserDictionary.
+     *
+     * @param word the word to add
+     * @param locale the locale of the word to add
+     * @param frequency the frequency of the word to add
+     * @return the Uri for the given word
+     */
+    @SuppressLint("NewApi")
+    private Uri addWord(final String word, final Locale locale, int frequency) {
+        // Add the given word for the given locale.
+        UserDictionary.Words.addWord(mContext, word, frequency, null, locale);
+        // Obtain an Uri for the given word.
+        Cursor cursor = mContentResolver.query(UserDictionary.Words.CONTENT_URI, null,
+                UserDictionary.Words.WORD + "='" + word + "'", null, null);
+        assertTrue(cursor.moveToFirst());
+        Uri uri = Uri.withAppendedPath(UserDictionary.Words.CONTENT_URI,
+                cursor.getString(cursor.getColumnIndex(UserDictionary.Words._ID)));
+        // Add the row to the backup for later clearing.
+        mAddedBackup.add(uri);
+        return uri;
+    }
+
+    /**
+     * Deletes the entry for the given word from UserDictionary.
+     *
+     * @param uri the Uri for the word as returned by addWord
+     */
+    private void deleteWord(Uri uri) {
+        // Remove the word from the backup so that it's not cleared again later.
+        mAddedBackup.remove(uri);
+        // Remove the word from UserDictionary.
+        mContentResolver.delete(uri, null, null);
+    }
+
+    public void testExactLocaleMatch() {
+        Log.d(TAG, "testExactLocaleMatch");
+
+        // Insert "Foo" as capitalized in the UserDictionary under en_US locale.
+        addWord("Foo", Locale.US, 17);
+
+        // Create the UserDictionaryLookup and wait until it's loaded.
+        UserDictionaryLookup lookup = new UserDictionaryLookup(mContext);
+        while (!lookup.isLoaded()) {
+        }
+
+        // Any capitalization variation should match.
+        assertTrue(lookup.isValidWord("foo", Locale.US));
+        assertTrue(lookup.isValidWord("Foo", Locale.US));
+        assertTrue(lookup.isValidWord("FOO", Locale.US));
+        // But similar looking words don't match.
+        assertFalse(lookup.isValidWord("fo", Locale.US));
+        assertFalse(lookup.isValidWord("fop", Locale.US));
+        assertFalse(lookup.isValidWord("fooo", Locale.US));
+        // Other locales, including more general locales won't match.
+        assertFalse(lookup.isValidWord("foo", Locale.ENGLISH));
+        assertFalse(lookup.isValidWord("foo", Locale.UK));
+        assertFalse(lookup.isValidWord("foo", Locale.FRENCH));
+        assertFalse(lookup.isValidWord("foo", new Locale("")));
+
+        lookup.close();
+    }
+
+    public void testSubLocaleMatch() {
+        Log.d(TAG, "testSubLocaleMatch");
+
+        // Insert "Foo" as capitalized in the UserDictionary under the en locale.
+        addWord("Foo", Locale.ENGLISH, 17);
+
+        // Create the UserDictionaryLookup and wait until it's loaded.
+        UserDictionaryLookup lookup = new UserDictionaryLookup(mContext);
+        while (!lookup.isLoaded()) {
+        }
+
+        // Any capitalization variation should match for both en and en_US.
+        assertTrue(lookup.isValidWord("foo", Locale.ENGLISH));
+        assertTrue(lookup.isValidWord("foo", Locale.US));
+        assertTrue(lookup.isValidWord("Foo", Locale.US));
+        assertTrue(lookup.isValidWord("FOO", Locale.US));
+        // But similar looking words don't match.
+        assertFalse(lookup.isValidWord("fo", Locale.US));
+        assertFalse(lookup.isValidWord("fop", Locale.US));
+        assertFalse(lookup.isValidWord("fooo", Locale.US));
+
+        lookup.close();
+    }
+
+    public void testAllLocalesMatch() {
+        Log.d(TAG, "testAllLocalesMatch");
+
+        // Insert "Foo" as capitalized in the UserDictionary under the all locales.
+        addWord("Foo", null, 17);
+
+        // Create the UserDictionaryLookup and wait until it's loaded.
+        UserDictionaryLookup lookup = new UserDictionaryLookup(mContext);
+        while (!lookup.isLoaded()) {
+        }
+
+        // Any capitalization variation should match for fr, en and en_US.
+        assertTrue(lookup.isValidWord("foo", new Locale("")));
+        assertTrue(lookup.isValidWord("foo", Locale.FRENCH));
+        assertTrue(lookup.isValidWord("foo", Locale.ENGLISH));
+        assertTrue(lookup.isValidWord("foo", Locale.US));
+        assertTrue(lookup.isValidWord("Foo", Locale.US));
+        assertTrue(lookup.isValidWord("FOO", Locale.US));
+        // But similar looking words don't match.
+        assertFalse(lookup.isValidWord("fo", Locale.US));
+        assertFalse(lookup.isValidWord("fop", Locale.US));
+        assertFalse(lookup.isValidWord("fooo", Locale.US));
+
+        lookup.close();
+    }
+
+    public void testMultipleLocalesMatch() {
+        Log.d(TAG, "testMultipleLocalesMatch");
+
+        // Insert "Foo" as capitalized in the UserDictionary under the en_US and en_CA and fr
+        // locales.
+        addWord("Foo", Locale.US, 17);
+        addWord("foO", Locale.CANADA, 17);
+        addWord("fOo", Locale.FRENCH, 17);
+
+        // Create the UserDictionaryLookup and wait until it's loaded.
+        UserDictionaryLookup lookup = new UserDictionaryLookup(mContext);
+        while (!lookup.isLoaded()) {
+        }
+
+        // Both en_CA and en_US match.
+        assertTrue(lookup.isValidWord("foo", Locale.CANADA));
+        assertTrue(lookup.isValidWord("foo", Locale.US));
+        assertTrue(lookup.isValidWord("foo", Locale.FRENCH));
+        // Other locales, including more general locales won't match.
+        assertFalse(lookup.isValidWord("foo", Locale.ENGLISH));
+        assertFalse(lookup.isValidWord("foo", Locale.UK));
+        assertFalse(lookup.isValidWord("foo", new Locale("")));
+
+        lookup.close();
+    }
+
+    public void testReload() {
+        Log.d(TAG, "testReload");
+
+        // Insert "foo".
+        Uri uri = addWord("foo", Locale.US, 17);
+
+        // Create the UserDictionaryLookup and wait until it's loaded.
+        UserDictionaryLookup lookup = new UserDictionaryLookup(mContext);
+        while (!lookup.isLoaded()) {
+        }
+
+        // "foo" should match.
+        assertTrue(lookup.isValidWord("foo", Locale.US));
+
+        // "bar" shouldn't match.
+        assertFalse(lookup.isValidWord("bar", Locale.US));
+
+        // Now delete "foo" and add "bar".
+        deleteWord(uri);
+        addWord("bar", Locale.US, 18);
+
+        // Wait a little bit before expecting a change. The time we wait should be greater than
+        // UserDictionaryLookup.RELOAD_DELAY_MS.
+        try {
+            Thread.sleep(UserDictionaryLookup.RELOAD_DELAY_MS + 1000);
+        } catch (InterruptedException e) {
+        }
+
+        // Perform lookups again. Reload should have occured.
+        //
+        // "foo" should not match.
+        assertFalse(lookup.isValidWord("foo", Locale.US));
+
+        // "bar" should match.
+        assertTrue(lookup.isValidWord("bar", Locale.US));
+
+        lookup.close();
+    }
+
+    public void testClose() {
+        Log.d(TAG, "testClose");
+
+        // Insert "foo".
+        Uri uri = addWord("foo", Locale.US, 17);
+
+        // Create the UserDictionaryLookup and wait until it's loaded.
+        UserDictionaryLookup lookup = new UserDictionaryLookup(mContext);
+        while (!lookup.isLoaded()) {
+        }
+
+        // "foo" should match.
+        assertTrue(lookup.isValidWord("foo", Locale.US));
+
+        // "bar" shouldn't match.
+        assertFalse(lookup.isValidWord("bar", Locale.US));
+
+        // Now close (prevents further reloads).
+        lookup.close();
+
+        // Now delete "foo" and add "bar".
+        deleteWord(uri);
+        addWord("bar", Locale.US, 18);
+
+        // Wait a little bit before expecting a change. The time we wait should be greater than
+        // UserDictionaryLookup.RELOAD_DELAY_MS.
+        try {
+            Thread.sleep(UserDictionaryLookup.RELOAD_DELAY_MS + 1000);
+        } catch (InterruptedException e) {
+        }
+
+        // Perform lookups again. Reload should not have occurred.
+        //
+        // "foo" should stil match.
+        assertTrue(lookup.isValidWord("foo", Locale.US));
+
+        // "bar" should still not match.
+        assertFalse(lookup.isValidWord("bar", Locale.US));
+    }
+}