Personal dictionary feeds a personal LM.

Bug 20043003.

Change-Id: I5ccac344c089855474263d1cdc547da1e6779301
diff --git a/java/src/com/android/inputmethod/latin/ContactsContentObserver.java b/java/src/com/android/inputmethod/latin/ContactsContentObserver.java
index 5eb9b16..561bac3 100644
--- a/java/src/com/android/inputmethod/latin/ContactsContentObserver.java
+++ b/java/src/com/android/inputmethod/latin/ContactsContentObserver.java
@@ -24,6 +24,7 @@
 import android.util.Log;
 
 import com.android.inputmethod.latin.ContactsManager.ContactsChangedListener;
+import com.android.inputmethod.latin.define.DebugFlags;
 import com.android.inputmethod.latin.utils.ExecutorUtils;
 
 import java.util.ArrayList;
@@ -33,8 +34,7 @@
  * A content observer that listens to updates to content provider {@link Contacts#CONTENT_URI}.
  */
 public class ContactsContentObserver implements Runnable {
-    private static final String TAG = ContactsContentObserver.class.getSimpleName();
-    private static final boolean DEBUG = false;
+    private static final String TAG = "ContactsContentObserver";
     private static AtomicBoolean sRunning = new AtomicBoolean(false);
 
     private final Context mContext;
@@ -49,8 +49,8 @@
     }
 
     public void registerObserver(final ContactsChangedListener listener) {
-        if (DEBUG) {
-            Log.d(TAG, "Registered Contacts Content Observer");
+        if (DebugFlags.DEBUG_ENABLED) {
+            Log.d(TAG, "registerObserver()");
         }
         mContactsChangedListener = listener;
         mContentObserver = new ContentObserver(null /* handler */) {
@@ -67,13 +67,13 @@
     @Override
     public void run() {
         if (!sRunning.compareAndSet(false /* expect */, true /* update */)) {
-            if (DEBUG) {
+            if (DebugFlags.DEBUG_ENABLED) {
                 Log.d(TAG, "run() : Already running. Don't waste time checking again.");
             }
             return;
         }
         if (haveContentsChanged()) {
-            if (DEBUG) {
+            if (DebugFlags.DEBUG_ENABLED) {
                 Log.d(TAG, "run() : Contacts have changed. Notifying listeners.");
             }
             mContactsChangedListener.onContactsChange();
@@ -91,9 +91,9 @@
             return false;
         }
         if (contactCount != mManager.getContactCountAtLastRebuild()) {
-            if (DEBUG) {
-                Log.d(TAG, "Contact count changed: " + mManager.getContactCountAtLastRebuild()
-                        + " to " + contactCount);
+            if (DebugFlags.DEBUG_ENABLED) {
+                Log.d(TAG, "haveContentsChanged() : Count changed from "
+                        + mManager.getContactCountAtLastRebuild() + " to " + contactCount);
             }
             return true;
         }
@@ -101,9 +101,9 @@
         if (names.hashCode() != mManager.getHashCodeAtLastRebuild()) {
             return true;
         }
-        if (DEBUG) {
-            Log.d(TAG, "No contacts changed. (runtime = " + (SystemClock.uptimeMillis() - startTime)
-                    + " ms)");
+        if (DebugFlags.DEBUG_ENABLED) {
+            Log.d(TAG, "haveContentsChanged() : No change detected in "
+                    + (SystemClock.uptimeMillis() - startTime) + " ms)");
         }
         return false;
     }
diff --git a/java/src/com/android/inputmethod/latin/ContactsManager.java b/java/src/com/android/inputmethod/latin/ContactsManager.java
index 1fadc6f..7a971cf 100644
--- a/java/src/com/android/inputmethod/latin/ContactsManager.java
+++ b/java/src/com/android/inputmethod/latin/ContactsManager.java
@@ -35,8 +35,7 @@
  * measure of the current state of the content provider.
  */
 public class ContactsManager {
-    private static final String TAG = ContactsManager.class.getSimpleName();
-    private static final boolean DEBUG = false;
+    private static final String TAG = "ContactsManager";
 
     /**
      * Interface to implement for classes interested in getting notified for updates
diff --git a/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java b/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java
index d5dff10..ff798ab 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java
@@ -45,31 +45,14 @@
 
     public static final String[] ALL_DICTIONARY_TYPES = new String[] {
             Dictionary.TYPE_MAIN,
+            Dictionary.TYPE_CONTACTS,
             Dictionary.TYPE_USER_HISTORY,
-            Dictionary.TYPE_USER,
-            Dictionary.TYPE_CONTACTS};
+            Dictionary.TYPE_USER};
 
     public static final String[] DYNAMIC_DICTIONARY_TYPES = new String[] {
+            Dictionary.TYPE_CONTACTS,
             Dictionary.TYPE_USER_HISTORY,
-            Dictionary.TYPE_USER,
-            Dictionary.TYPE_CONTACTS};
-
-    /**
-     * {@link Dictionary#TYPE_USER} is deprecated, except for the spelling service.
-     */
-    public static final String[] DICTIONARY_TYPES_FOR_SPELLING = new String[] {
-            Dictionary.TYPE_MAIN,
-            Dictionary.TYPE_USER_HISTORY,
-            Dictionary.TYPE_USER,
-            Dictionary.TYPE_CONTACTS};
-
-    /**
-     * {@link Dictionary#TYPE_USER} is deprecated, except for the spelling service.
-     */
-    public static final String[] DICTIONARY_TYPES_FOR_SUGGESTIONS = new String[] {
-            Dictionary.TYPE_MAIN,
-            Dictionary.TYPE_USER_HISTORY,
-            Dictionary.TYPE_CONTACTS};
+            Dictionary.TYPE_USER};
 
     /**
      * Returns whether this facilitator is exactly for this locale.
diff --git a/java/src/com/android/inputmethod/latin/DictionaryFacilitatorImpl.java b/java/src/com/android/inputmethod/latin/DictionaryFacilitatorImpl.java
index 9ce92da..7233d27 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryFacilitatorImpl.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitatorImpl.java
@@ -557,7 +557,7 @@
                 false /* firstSuggestionExceedsConfidenceThreshold */);
         final float[] weightOfLangModelVsSpatialModel =
                 new float[] { Dictionary.NOT_A_WEIGHT_OF_LANG_MODEL_VS_SPATIAL_MODEL };
-        for (final String dictType : DICTIONARY_TYPES_FOR_SUGGESTIONS) {
+        for (final String dictType : ALL_DICTIONARY_TYPES) {
             final Dictionary dictionary = mDictionaryGroup.getDict(dictType);
             if (null == dictionary) continue;
             final float weightForLocale = composedData.mIsBatchMode
@@ -577,11 +577,11 @@
     }
 
     public boolean isValidSpellingWord(final String word) {
-        return isValidWord(word, DICTIONARY_TYPES_FOR_SPELLING);
+        return isValidWord(word, ALL_DICTIONARY_TYPES);
     }
 
     public boolean isValidSuggestionWord(final String word) {
-        return isValidWord(word, DICTIONARY_TYPES_FOR_SUGGESTIONS);
+        return isValidWord(word, ALL_DICTIONARY_TYPES);
     }
 
     private boolean isValidWord(final String word, final String[] dictionariesToCheck) {
diff --git a/java/src/com/android/inputmethod/latin/PersonalDictionaryLookup.java b/java/src/com/android/inputmethod/latin/PersonalDictionaryLookup.java
new file mode 100644
index 0000000..1ba075c
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/PersonalDictionaryLookup.java
@@ -0,0 +1,649 @@
+/*
+ * 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;
+
+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.text.TextUtils;
+import android.util.Log;
+
+import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.latin.common.CollectionUtils;
+import com.android.inputmethod.latin.common.LocaleUtils;
+import com.android.inputmethod.latin.define.DebugFlags;
+import com.android.inputmethod.latin.utils.ExecutorUtils;
+
+import java.io.Closeable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/**
+ * This class provides the ability to look into the system-wide "Personal dictionary". It loads the
+ * data once when created and reloads it when notified of changes to {@link UserDictionary}
+ *
+ * It can be used directly to validate words or expand shortcuts, and it can be used by instances
+ * of {@link PersonalLanguageModelHelper} that create language model files for a specific input
+ * locale.
+ *
+ * Note, that the initial dictionary loading happens asynchronously so it is possible (hopefully
+ * rarely) that {@link #isValidWord} or {@link #expandShortcut} is called before the initial load
+ * has started.
+ *
+ * The caller should explicitly call {@link #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
+ * {@link android.app.Service#onCreate} and close it in {@link android.app.Service#onDestroy}.
+ */
+public class PersonalDictionaryLookup implements Closeable {
+
+    /**
+     * 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;
+
+    @UsedForTesting
+    static final Locale ANY_LOCALE = new Locale("");
+
+    private final String mTag;
+    private final ContentResolver mResolver;
+    private final String mServiceName;
+
+    /**
+     * Interface to implement for classes interested in getting notified of updates.
+     */
+    public static interface PersonalDictionaryListener {
+        public void onUpdate();
+    }
+
+    private final Set<PersonalDictionaryListener> mListeners = new HashSet<>();
+
+    public void addListener(@Nonnull final PersonalDictionaryListener listener) {
+        mListeners.add(listener);
+    }
+
+    public void removeListener(@Nonnull final PersonalDictionaryListener listener) {
+        mListeners.remove(listener);
+    }
+
+    /**
+     * Broadcast the update to all the Locale-specific language models.
+     */
+    @UsedForTesting
+    void notifyListeners() {
+        for (PersonalDictionaryListener listener : mListeners) {
+            listener.onUpdate();
+        }
+    }
+
+    /**
+     *  Content observer for changes to the personal dictionary. It has the following properties:
+     *    1. It spawns off a 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 the dictionary is edited through its settings UI, when sometimes multiple
+     *       notifications are sent for the edited entry, but also for the entire dictionary).
+     */
+    private class PersonalDictionaryContentObserver extends ContentObserver implements Runnable {
+        public PersonalDictionaryContentObserver() {
+            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 (DebugFlags.DEBUG_ENABLED) {
+                Log.d(mTag, "onChange() : 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 (DebugFlags.DEBUG_ENABLED) {
+                    if (isCancelled) {
+                        Log.d(mTag, "onChange() : Canceled previous reload request");
+                    } else {
+                        Log.d(mTag, "onChange() : Failed to cancel previous reload request");
+                    }
+                }
+            }
+
+            if (DebugFlags.DEBUG_ENABLED) {
+                Log.d(mTag, "onChange() : Scheduling reload in " + RELOAD_DELAY_MS + " ms");
+            }
+
+            // Schedule a new reload after RELOAD_DELAY_MS.
+            mReloadFuture = ExecutorUtils.getBackgroundExecutor(mServiceName)
+                    .schedule(this, RELOAD_DELAY_MS, TimeUnit.MILLISECONDS);
+        }
+
+        @Override
+        public void run() {
+            loadPersonalDictionary();
+        }
+    }
+
+    private final PersonalDictionaryContentObserver mPersonalDictionaryContentObserver =
+            new PersonalDictionaryContentObserver();
+
+    /**
+     * 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;
+
+    /**
+     * We store a map from a shortcut to a word for each locale.
+     * Shortcuts that apply to any locale are keyed by {@link #ANY_LOCALE}.
+     */
+    private volatile HashMap<Locale, HashMap<String, String>> mShortcutsPerLocale;
+
+    /**
+     *  The last-scheduled reload future.  Saved in order to cancel a pending reload if a new one
+     * is coming.
+     */
+    private volatile ScheduledFuture<?> mReloadFuture;
+
+    private volatile List<DictionaryStats> mDictionaryStats;
+
+    /**
+     * @param context the context from which to obtain content resolver
+     */
+    public PersonalDictionaryLookup(
+            @Nonnull final Context context,
+            @Nonnull final String serviceName) {
+        mTag = serviceName + ".Personal";
+
+        Log.i(mTag, "create()");
+
+        mServiceName = serviceName;
+        mDictionaryStats = new ArrayList<DictionaryStats>();
+        mDictionaryStats.add(new DictionaryStats(ANY_LOCALE, Dictionary.TYPE_USER, 0));
+        mDictionaryStats.add(new DictionaryStats(ANY_LOCALE, Dictionary.TYPE_USER_SHORTCUT, 0));
+
+        // Obtain a content resolver.
+        mResolver = context.getContentResolver();
+    }
+
+    public List<DictionaryStats> getDictionaryStats() {
+        return mDictionaryStats;
+    }
+
+    public void open() {
+        Log.i(mTag, "open()");
+
+        // Schedule the initial load to run immediately.  It's possible that the first call to
+        // isValidWord occurs before the dictionary has actually loaded, so it should not
+        // assume that the dictionary has been loaded.
+        loadPersonalDictionary();
+
+        // Register the observer to be notified on changes to the personal dictionary and all
+        // individual items.
+        //
+        // If the user is interacting with the Personal Dictionary 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 */,
+                mPersonalDictionaryContentObserver);
+    }
+
+    /**
+     * 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 (DebugFlags.DEBUG_ENABLED) {
+                Log.d(mTag, "finalize()");
+            }
+            close();
+        } finally {
+            super.finalize();
+        }
+    }
+
+    /**
+     * Cleans up PersonalDictionaryLookup: 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 (DebugFlags.DEBUG_ENABLED) {
+            Log.d(mTag, "close() : Unregistering content observer");
+        }
+        if (mIsClosed.compareAndSet(false, true)) {
+            // Unregister the content observer.
+            mResolver.unregisterContentObserver(mPersonalDictionaryContentObserver);
+        }
+    }
+
+    /**
+     * Returns true if the initial load has been performed.
+     *
+     * @return true if the initial load is successful
+     */
+    public boolean isLoaded() {
+        return mDictWords != null && mShortcutsPerLocale != null;
+    }
+
+    /**
+     * Returns the set of words defined for the given locale and more general locales.
+     *
+     * For example, input locale en_US uses data for en_US, en, and the global dictionary.
+     *
+     * Note that this method returns expanded words, not shortcuts. Shortcuts are handled
+     * by {@link #getShortcutsForLocale}.
+     *
+     * @param inputLocale the locale to restrict for
+     * @return set of words that apply to the given locale.
+     */
+    public Set<String> getWordsForLocale(@Nonnull final Locale inputLocale) {
+        final HashMap<String, ArrayList<Locale>> dictWords = mDictWords;
+        if (CollectionUtils.isNullOrEmpty(dictWords)) {
+            return Collections.emptySet();
+        }
+
+        final Set<String> words = new HashSet<>();
+        final String inputLocaleString = inputLocale.toString();
+        for (String word : dictWords.keySet()) {
+            for (Locale wordLocale : dictWords.get(word)) {
+                final String wordLocaleString = wordLocale.toString();
+                final int match = LocaleUtils.getMatchLevel(wordLocaleString, inputLocaleString);
+                if (LocaleUtils.isMatch(match)) {
+                    words.add(word);
+                }
+            }
+        }
+        return words;
+    }
+
+    /**
+     * Returns the set of shortcuts defined for the given locale and more general locales.
+     *
+     * For example, input locale en_US uses data for en_US, en, and the global dictionary.
+     *
+     * Note that this method returns shortcut keys, not expanded words. Words are handled
+     * by {@link #getWordsForLocale}.
+     *
+     * @param inputLocale the locale to restrict for
+     * @return set of shortcuts that apply to the given locale.
+     */
+    public Set<String> getShortcutsForLocale(@Nonnull final Locale inputLocale) {
+        final Map<Locale, HashMap<String, String>> shortcutsPerLocale = mShortcutsPerLocale;
+        if (CollectionUtils.isNullOrEmpty(shortcutsPerLocale)) {
+            return Collections.emptySet();
+        }
+
+        final Set<String> shortcuts = new HashSet<>();
+        if (!TextUtils.isEmpty(inputLocale.getCountry())) {
+            // First look for the country-specific shortcut: en_US, en_UK, fr_FR, etc.
+            final Map<String, String> countryShortcuts = shortcutsPerLocale.get(inputLocale);
+            if (!CollectionUtils.isNullOrEmpty(countryShortcuts)) {
+                shortcuts.addAll(countryShortcuts.keySet());
+            }
+        }
+
+        // Next look for the language-specific shortcut: en, fr, etc.
+        final Locale languageOnlyLocale =
+                LocaleUtils.constructLocaleFromString(inputLocale.getLanguage());
+        final Map<String, String> languageShortcuts = shortcutsPerLocale.get(languageOnlyLocale);
+        if (!CollectionUtils.isNullOrEmpty(languageShortcuts)) {
+            shortcuts.addAll(languageShortcuts.keySet());
+        }
+
+        // If all else fails, look for a global shortcut.
+        final Map<String, String> globalShortcuts = shortcutsPerLocale.get(ANY_LOCALE);
+        if (!CollectionUtils.isNullOrEmpty(globalShortcuts)) {
+            shortcuts.addAll(globalShortcuts.keySet());
+        }
+
+        return shortcuts;
+    }
+
+    /**
+     * Determines if the given word is a valid word in the given locale based on the dictionary.
+     * 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 inputLocale the locale in which to match the word
+     * @return true iff the word has been matched for this locale in the dictionary.
+     */
+    public boolean isValidWord(@Nonnull final String word, @Nonnull final Locale inputLocale) {
+        if (!isLoaded()) {
+            // This is a corner case in the event the initial load of the dictionary has not
+            // completed. In that case, we assume the word is not a valid word in the dictionary.
+            if (DebugFlags.DEBUG_ENABLED) {
+                Log.d(mTag, "isValidWord() : Initial load not complete");
+            }
+            return false;
+        }
+
+        // Atomically obtain the current copy of mDictWords;
+        final HashMap<String, ArrayList<Locale>> dictWords = mDictWords;
+
+        if (DebugFlags.DEBUG_ENABLED) {
+            Log.d(mTag, "isValidWord() : Word [" + word + "] in Locale [" + inputLocale + "]");
+        }
+        // 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(inputLocale);
+        final ArrayList<Locale> dictLocales = dictWords.get(lowercased);
+        if (null == dictLocales) {
+            if (DebugFlags.DEBUG_ENABLED) {
+                Log.d(mTag, "isValidWord() : No entry for lowercased word [" + lowercased + "]");
+            }
+            return false;
+        } else {
+            if (DebugFlags.DEBUG_ENABLED) {
+                Log.d(mTag, "isValidWord() : Found entry for lowercased word [" + lowercased + "]");
+            }
+            // Iterate over the locales this word is in.
+            for (final Locale dictLocale : dictLocales) {
+                final int matchLevel = LocaleUtils.getMatchLevel(dictLocale.toString(),
+                        inputLocale.toString());
+                if (DebugFlags.DEBUG_ENABLED) {
+                    Log.d(mTag, "isValidWord() : MatchLevel for DictLocale [" + dictLocale
+                            + "] and InputLocale [" + inputLocale + "] is " + matchLevel);
+                }
+                if (LocaleUtils.isMatch(matchLevel)) {
+                    if (DebugFlags.DEBUG_ENABLED) {
+                        Log.d(mTag, "isValidWord() : MatchLevel " + matchLevel + " IS a match");
+                    }
+                    return true;
+                }
+                if (DebugFlags.DEBUG_ENABLED) {
+                    Log.d(mTag, "isValidWord() : MatchLevel " + matchLevel + " is NOT a match");
+                }
+            }
+            if (DebugFlags.DEBUG_ENABLED) {
+                Log.d(mTag, "isValidWord() : False, since none of the locales matched");
+            }
+            return false;
+        }
+    }
+
+    /**
+     * Expands the given shortcut for the given locale.
+     *
+     * @param shortcut the shortcut to expand
+     * @param inputLocale the locale in which to expand the shortcut
+     * @return expanded shortcut iff the word is a shortcut in the dictionary.
+     */
+    @Nullable public String expandShortcut(
+            @Nonnull final String shortcut, @Nonnull final Locale inputLocale) {
+        if (DebugFlags.DEBUG_ENABLED) {
+            Log.d(mTag, "expandShortcut() : Shortcut [" + shortcut + "] for [" + inputLocale + "]");
+        }
+
+        // Atomically obtain the current copy of mShortcuts;
+        final HashMap<Locale, HashMap<String, String>> shortcutsPerLocale = mShortcutsPerLocale;
+
+        // Exit as early as possible. Most users don't use shortcuts.
+        if (CollectionUtils.isNullOrEmpty(shortcutsPerLocale)) {
+            if (DebugFlags.DEBUG_ENABLED) {
+                Log.d(mTag, "expandShortcut() : User has no shortcuts");
+            }
+            return null;
+        }
+
+        if (!TextUtils.isEmpty(inputLocale.getCountry())) {
+            // First look for the country-specific shortcut: en_US, en_UK, fr_FR, etc.
+            final String expansionForCountry = expandShortcut(
+                    shortcutsPerLocale, shortcut, inputLocale);
+            if (!TextUtils.isEmpty(expansionForCountry)) {
+                if (DebugFlags.DEBUG_ENABLED) {
+                    Log.d(mTag, "expandShortcut() : Country expansion is ["
+                            + expansionForCountry + "]");
+                }
+                return expansionForCountry;
+            }
+        }
+
+        // Next look for the language-specific shortcut: en, fr, etc.
+        final Locale languageOnlyLocale =
+                LocaleUtils.constructLocaleFromString(inputLocale.getLanguage());
+        final String expansionForLanguage = expandShortcut(
+                shortcutsPerLocale, shortcut, languageOnlyLocale);
+        if (!TextUtils.isEmpty(expansionForLanguage)) {
+            if (DebugFlags.DEBUG_ENABLED) {
+                Log.d(mTag, "expandShortcut() : Language expansion is ["
+                        + expansionForLanguage + "]");
+            }
+            return expansionForLanguage;
+        }
+
+        // If all else fails, look for a global shortcut.
+        final String expansionForGlobal = expandShortcut(shortcutsPerLocale, shortcut, ANY_LOCALE);
+        if (!TextUtils.isEmpty(expansionForGlobal) && DebugFlags.DEBUG_ENABLED) {
+            Log.d(mTag, "expandShortcut() : Global expansion is [" + expansionForGlobal + "]");
+        }
+        return expansionForGlobal;
+    }
+
+    @Nullable private String expandShortcut(
+            @Nullable final HashMap<Locale, HashMap<String, String>> shortcutsPerLocale,
+            @Nonnull final String shortcut,
+            @Nonnull final Locale locale) {
+        if (CollectionUtils.isNullOrEmpty(shortcutsPerLocale)) {
+            return null;
+        }
+        final HashMap<String, String> localeShortcuts = shortcutsPerLocale.get(locale);
+        if (CollectionUtils.isNullOrEmpty(localeShortcuts)) {
+            return null;
+        }
+        return localeShortcuts.get(shortcut);
+    }
+
+    /**
+     * Loads the personal dictionary in the current thread.
+     *
+     * Only one reload can happen at a time. If already running, will exit quickly.
+     */
+    private void loadPersonalDictionary() {
+        // Bail out if already in the process of loading.
+        if (!mIsLoading.compareAndSet(false, true)) {
+            Log.i(mTag, "loadPersonalDictionary() : Already Loading (exit)");
+            return;
+        }
+        Log.i(mTag, "loadPersonalDictionary() : Start Loading");
+        HashMap<String, ArrayList<Locale>> dictWords = new HashMap<>();
+        HashMap<Locale, HashMap<String, String>> shortcutsPerLocale = new HashMap<>();
+        // Load the dictionary.  Items are returned in the default sort order (by frequency).
+        Cursor cursor = mResolver.query(UserDictionary.Words.CONTENT_URI,
+                null, null, null, UserDictionary.Words.DEFAULT_SORT_ORDER);
+        if (null == cursor || cursor.getCount() < 1) {
+            Log.i(mTag, "loadPersonalDictionary() : Empty");
+        } else {
+            // Iterate over the entries in the personal dictionary.  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 (DebugFlags.DEBUG_ENABLED) {
+                        Log.d(mTag, "loadPersonalDictionary() : 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 (DebugFlags.DEBUG_ENABLED) {
+                        Log.d(mTag, "loadPersonalDictionary() : Entry without WORD, skipping");
+                    }
+                    continue;
+                }
+                // If the word is null, skip this entry.
+                final String rawDictWord = cursor.getString(dictWordIndex);
+                if (null == rawDictWord) {
+                    if (DebugFlags.DEBUG_ENABLED) {
+                        Log.d(mTag, "loadPersonalDictionary() : 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 (DebugFlags.DEBUG_ENABLED) {
+                        Log.d(mTag, "loadPersonalDictionary() : 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 (DebugFlags.DEBUG_ENABLED) {
+                    Log.d(mTag, "loadPersonalDictionary() : Adding 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 (DebugFlags.DEBUG_ENABLED) {
+                        Log.d(mTag, "loadPersonalDictionary() : Word [" + dictWord +
+                                "] not seen for other locales, creating new entry");
+                    }
+                    dictLocales = new ArrayList<>();
+                    dictWords.put(dictWord, dictLocales);
+                }
+                // Append the locale to the list of locales this word is in.
+                dictLocales.add(dictLocale);
+
+                // If there is no column for a shortcut, we're done.
+                final int shortcutIndex = cursor.getColumnIndex(UserDictionary.Words.SHORTCUT);
+                if (shortcutIndex < 0) {
+                    if (DebugFlags.DEBUG_ENABLED) {
+                        Log.d(mTag, "loadPersonalDictionary() : Entry without SHORTCUT, done");
+                    }
+                    continue;
+                }
+                // If the shortcut is null, we're done.
+                final String shortcut = cursor.getString(shortcutIndex);
+                if (shortcut == null) {
+                    if (DebugFlags.DEBUG_ENABLED) {
+                        Log.d(mTag, "loadPersonalDictionary() : Null shortcut");
+                    }
+                    continue;
+                }
+                // Else, save the shortcut.
+                HashMap<String, String> localeShortcuts = shortcutsPerLocale.get(dictLocale);
+                if (localeShortcuts == null) {
+                    localeShortcuts = new HashMap<>();
+                    shortcutsPerLocale.put(dictLocale, localeShortcuts);
+                }
+                // Map to the raw input, which might be capitalized.
+                // This lets the user create a shortcut from "gm" to "General Motors".
+                localeShortcuts.put(shortcut, rawDictWord);
+            }
+        }
+
+        List<DictionaryStats> stats = new ArrayList<>();
+        stats.add(new DictionaryStats(ANY_LOCALE, Dictionary.TYPE_USER, dictWords.size()));
+        int numShortcuts = 0;
+        for (HashMap<String, String> shortcuts : shortcutsPerLocale.values()) {
+            numShortcuts += shortcuts.size();
+        }
+        stats.add(new DictionaryStats(ANY_LOCALE, Dictionary.TYPE_USER_SHORTCUT, numShortcuts));
+        mDictionaryStats = stats;
+
+        // Atomically replace the copy of mDictWords and mShortcuts.
+        mDictWords = dictWords;
+        mShortcutsPerLocale = shortcutsPerLocale;
+
+        // Allow other calls to loadPersonalDictionary to execute now.
+        mIsLoading.set(false);
+
+        Log.i(mTag, "loadPersonalDictionary() : Loaded " + mDictWords.size()
+                + " words and " + numShortcuts + " shortcuts");
+
+        notifyListeners();
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/UserDictionaryLookup.java b/java/src/com/android/inputmethod/latin/UserDictionaryLookup.java
deleted file mode 100644
index 1eed3d7..0000000
--- a/java/src/com/android/inputmethod/latin/UserDictionaryLookup.java
+++ /dev/null
@@ -1,538 +0,0 @@
-/*
- * 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;
-
-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.text.TextUtils;
-import android.util.Log;
-
-import com.android.inputmethod.annotations.UsedForTesting;
-import com.android.inputmethod.latin.common.CollectionUtils;
-import com.android.inputmethod.latin.common.LocaleUtils;
-import com.android.inputmethod.latin.utils.ExecutorUtils;
-
-import java.io.Closeable;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Locale;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.TimeUnit;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-/**
- * UserDictionaryLookup provides the ability to lookup into the system-wide "Personal dictionary".
- *
- * 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 {
-
-    /**
-     * 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;
-
-    @UsedForTesting
-    static final Locale ANY_LOCALE = new Locale("");
-
-    private final String mTag;
-    private final ContentResolver mResolver;
-    private final String mServiceName;
-
-    /**
-     * Runnable that calls loadUserDictionary().
-     */
-    private class UserDictionaryLoader implements Runnable {
-        @Override
-        public void run() {
-            if (DEBUG) {
-                Log.d(mTag, "Executing (re)load");
-            }
-            loadUserDictionary();
-        }
-    }
-
-    /**
-     *  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(mTag, "Received content observer onChange notification for URI: " + uri);
-            }
-            // Cancel (but don't interrupt) any pending reloads (except the initial load).
-            if (mReloadFuture != null && !mReloadFuture.isCancelled() &&
-                    !mReloadFuture.isDone()) {
-                // Note, that if already cancelled or done, this will do nothing.
-                boolean isCancelled = mReloadFuture.cancel(false);
-                if (DEBUG) {
-                    if (isCancelled) {
-                        Log.d(mTag, "Successfully canceled previous reload request");
-                    } else {
-                        Log.d(mTag, "Unable to cancel previous reload request");
-                    }
-                }
-            }
-
-            if (DEBUG) {
-                Log.d(mTag, "Scheduling reload in " + RELOAD_DELAY_MS + " ms");
-            }
-
-            // Schedule a new reload after RELOAD_DELAY_MS.
-            mReloadFuture = ExecutorUtils.getBackgroundExecutor(mServiceName)
-                    .schedule(new UserDictionaryLoader(), 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;
-
-    /**
-     * We store a map from a shortcut to a word for each locale.
-     * Shortcuts that apply to any locale are keyed by {@link #ANY_LOCALE}.
-     */
-    private volatile HashMap<Locale, HashMap<String, String>> mShortcutsPerLocale;
-
-    /**
-     *  The last-scheduled reload future.  Saved in order to cancel a pending reload if a new one
-     * is coming.
-     */
-    private volatile ScheduledFuture<?> mReloadFuture;
-
-    private volatile List<DictionaryStats> mDictionaryStats;
-
-    /**
-     * @param context the context from which to obtain content resolver
-     */
-    public UserDictionaryLookup(@Nonnull final Context context, @Nonnull final String serviceName) {
-        mTag = serviceName + ".UserDictionaryLookup";
-
-        Log.i(mTag, "create()");
-
-        mServiceName = serviceName;
-        mDictionaryStats = new ArrayList<DictionaryStats>();
-        mDictionaryStats.add(new DictionaryStats(ANY_LOCALE, Dictionary.TYPE_USER, 0));
-        mDictionaryStats.add(new DictionaryStats(ANY_LOCALE, Dictionary.TYPE_USER_SHORTCUT, 0));
-
-        // Obtain a content resolver.
-        mResolver = context.getContentResolver();
-    }
-
-    public List<DictionaryStats> getDictionaryStats() {
-        return mDictionaryStats;
-    }
-
-    public void open() {
-        Log.i(mTag, "open()");
-
-        // Schedule the initial load to run immediately.  It's possible that the first call to
-        // isValidWord occurs before the dictionary has actually loaded, so it should not
-        // assume that the dictionary has been loaded.
-        loadUserDictionary();
-
-        // 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(mTag, "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(mTag, "Close called (no pun intended), cleaning up executor and observer");
-        }
-        if (mIsClosed.compareAndSet(false, true)) {
-            // Unregister the content observer.
-            mResolver.unregisterContentObserver(mObserver);
-        }
-    }
-
-    /**
-     * Returns true if the initial load has been performed.
-     *
-     * @return true if the initial load is successful
-     */
-    public boolean isLoaded() {
-        return mDictWords != null && mShortcutsPerLocale != 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(@Nonnull final String word, @Nonnull final Locale locale) {
-        if (!isLoaded()) {
-            // This is a corner case in the event the initial load of UserDictionary has not
-            // been loaded. In that case, we assume the word is not a valid word in
-            // UserDictionary.
-            if (DEBUG) {
-                Log.d(mTag, "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(mTag, "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(mTag, "isValidWord=false, since there is no entry for " +
-                        "lowercased word [" + lowercased + "]");
-            }
-            return false;
-        } else {
-            if (DEBUG) {
-                Log.d(mTag, "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(mTag, "matchLevel for dictLocale=" + dictLocale + ", locale=" +
-                            locale + " is " + matchLevel);
-                }
-                if (LocaleUtils.isMatch(matchLevel)) {
-                    if (DEBUG) {
-                        Log.d(mTag, "isValidWord=true, since matchLevel " + matchLevel +
-                                " is a match");
-                    }
-                    return true;
-                }
-                if (DEBUG) {
-                    Log.d(mTag, "matchLevel " + matchLevel + " is not a match");
-                }
-            }
-            if (DEBUG) {
-                Log.d(mTag, "isValidWord=false, since none of the locales matched");
-            }
-            return false;
-        }
-    }
-
-    /**
-     * Expands the given shortcut for the given locale.
-     *
-     * @param shortcut the shortcut to expand
-     * @param inputLocale the locale in which to expand the shortcut
-     * @return expanded shortcut iff the word is a shortcut in the UserDictionary.
-     */
-    @Nullable public String expandShortcut(
-            @Nonnull final String shortcut, @Nonnull final Locale inputLocale) {
-        if (DEBUG) {
-            Log.d(mTag, "expandShortcut() : Shortcut [" + shortcut + "] for [" + inputLocale + "]");
-        }
-
-        // Atomically obtain the current copy of mShortcuts;
-        final HashMap<Locale, HashMap<String, String>> shortcutsPerLocale = mShortcutsPerLocale;
-
-        // Exit as early as possible. Most users don't use shortcuts.
-        if (CollectionUtils.isNullOrEmpty(shortcutsPerLocale)) {
-            return null;
-        }
-
-        if (!TextUtils.isEmpty(inputLocale.getCountry())) {
-            // First look for the country-specific shortcut: en_US, en_UK, fr_FR, etc.
-            final String expansionForCountry = expandShortcut(
-                    shortcutsPerLocale, shortcut, inputLocale);
-            if (!TextUtils.isEmpty(expansionForCountry)) {
-                return expansionForCountry;
-            }
-        }
-
-        // Next look for the language-specific shortcut: en, fr, etc.
-        final Locale languageOnlyLocale =
-                LocaleUtils.constructLocaleFromString(inputLocale.getLanguage());
-        final String expansionForLanguage = expandShortcut(
-                shortcutsPerLocale, shortcut, languageOnlyLocale);
-        if (!TextUtils.isEmpty(expansionForLanguage)) {
-            return expansionForLanguage;
-        }
-
-        // If all else fails, loof for a global shortcut.
-        return expandShortcut(shortcutsPerLocale, shortcut, ANY_LOCALE);
-    }
-
-    @Nullable private String expandShortcut(
-            @Nullable final HashMap<Locale, HashMap<String, String>> shortcutsPerLocale,
-            @Nonnull final String shortcut,
-            @Nonnull final Locale locale) {
-        if (CollectionUtils.isNullOrEmpty(shortcutsPerLocale)) {
-            return null;
-        }
-        final HashMap<String, String> localeShortcuts = shortcutsPerLocale.get(locale);
-        if (CollectionUtils.isNullOrEmpty(localeShortcuts)) {
-            return null;
-        }
-        final String word = localeShortcuts.get(shortcut);
-        if (DEBUG && word != null) {
-            Log.d(mTag, "expandShortcut() : Shortcut [" + shortcut + "] for [" + locale
-                    + "] expands to [" + word + "]");
-        }
-        return word;
-    }
-
-    /**
-     * Loads the UserDictionary in the current thread.
-     *
-     * Only one reload can happen at a time. If already running, will exit quickly.
-     */
-    private void loadUserDictionary() {
-        // Bail out if already in the process of loading.
-        if (!mIsLoading.compareAndSet(false, true)) {
-            Log.i(mTag, "loadUserDictionary() : Already Loading (exit)");
-            return;
-        }
-        Log.i(mTag, "loadUserDictionary() : Start Loading");
-        HashMap<String, ArrayList<Locale>> dictWords = new HashMap<>();
-        HashMap<Locale, HashMap<String, String>> shortcutsPerLocale = new HashMap<>();
-        // Load the UserDictionary.  Request that items be returned in the default sort order
-        // for UserDictionary, which is by frequency.
-        Cursor cursor = mResolver.query(UserDictionary.Words.CONTENT_URI,
-                null, null, null, UserDictionary.Words.DEFAULT_SORT_ORDER);
-        if (null == cursor || cursor.getCount() < 1) {
-            Log.i(mTag, "loadUserDictionary() : Empty");
-        } else {
-            // Iterate over the entries in the UserDictionary.  Note, that iteration is in
-            // descending frequency by default.
-            while (dictWords.size() < MAX_NUM_ENTRIES && cursor.moveToNext()) {
-                // If there is no column for locale, skip this entry. An empty
-                // locale on the other hand will not be skipped.
-                final int dictLocaleIndex = cursor.getColumnIndex(UserDictionary.Words.LOCALE);
-                if (dictLocaleIndex < 0) {
-                    if (DEBUG) {
-                        Log.d(mTag, "Encountered UserDictionary entry without LOCALE, skipping");
-                    }
-                    continue;
-                }
-                // If there is no column for word, skip this entry.
-                final int dictWordIndex = cursor.getColumnIndex(UserDictionary.Words.WORD);
-                if (dictWordIndex < 0) {
-                    if (DEBUG) {
-                        Log.d(mTag, "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(mTag, "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(mTag, "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(mTag, "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(mTag, "Word [" + dictWord +
-                                "] not seen for other locales, creating new entry");
-                    }
-                    dictLocales = new ArrayList<>();
-                    dictWords.put(dictWord, dictLocales);
-                }
-                // Append the locale to the list of locales this word is in.
-                dictLocales.add(dictLocale);
-
-                // If there is no column for a shortcut, we're done.
-                final int shortcutIndex = cursor.getColumnIndex(UserDictionary.Words.SHORTCUT);
-                if (shortcutIndex < 0) {
-                    if (DEBUG) {
-                        Log.d(mTag, "Encountered UserDictionary entry without SHORTCUT, done");
-                    }
-                    continue;
-                }
-                // If the shortcut is null, we're done.
-                final String shortcut = cursor.getString(shortcutIndex);
-                if (shortcut == null) {
-                    if (DEBUG) {
-                        Log.d(mTag, "Encountered null shortcut");
-                    }
-                    continue;
-                }
-                // Else, save the shortcut.
-                HashMap<String, String> localeShortcuts = shortcutsPerLocale.get(dictLocale);
-                if (localeShortcuts == null) {
-                    localeShortcuts = new HashMap<>();
-                    shortcutsPerLocale.put(dictLocale, localeShortcuts);
-                }
-                // Map to the raw input, which might be capitalized.
-                // This lets the user create a shortcut from "gm" to "General Motors".
-                localeShortcuts.put(shortcut, rawDictWord);
-            }
-        }
-
-        List<DictionaryStats> stats = new ArrayList<>();
-        stats.add(new DictionaryStats(ANY_LOCALE, Dictionary.TYPE_USER, dictWords.size()));
-        int numShortcuts = 0;
-        for (HashMap<String, String> shortcuts : shortcutsPerLocale.values()) {
-            numShortcuts += shortcuts.size();
-        }
-        stats.add(new DictionaryStats(ANY_LOCALE, Dictionary.TYPE_USER_SHORTCUT, numShortcuts));
-        mDictionaryStats = stats;
-
-        // Atomically replace the copy of mDictWords and mShortcuts.
-        mDictWords = dictWords;
-        mShortcutsPerLocale = shortcutsPerLocale;
-
-        // Allow other calls to loadUserDictionary to execute now.
-        mIsLoading.set(false);
-
-        Log.i(mTag, "loadUserDictionary() : Loaded " + mDictWords.size()
-                + " words and " + numShortcuts + " shortcuts");
-    }
-}
diff --git a/tests/src/com/android/inputmethod/latin/UserDictionaryLookupTest.java b/tests/src/com/android/inputmethod/latin/PersonalDictionaryLookupTest.java
similarity index 63%
rename from tests/src/com/android/inputmethod/latin/UserDictionaryLookupTest.java
rename to tests/src/com/android/inputmethod/latin/PersonalDictionaryLookupTest.java
index 917140a..983957f 100644
--- a/tests/src/com/android/inputmethod/latin/UserDictionaryLookupTest.java
+++ b/tests/src/com/android/inputmethod/latin/PersonalDictionaryLookupTest.java
@@ -16,7 +16,12 @@
 
 package com.android.inputmethod.latin;
 
-import static com.android.inputmethod.latin.UserDictionaryLookup.ANY_LOCALE;
+import static com.android.inputmethod.latin.PersonalDictionaryLookup.ANY_LOCALE;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
 
 import android.annotation.SuppressLint;
 import android.content.ContentResolver;
@@ -27,20 +32,22 @@
 import android.test.suitebuilder.annotation.SmallTest;
 import android.util.Log;
 
+import com.android.inputmethod.latin.PersonalDictionaryLookup.PersonalDictionaryListener;
 import com.android.inputmethod.latin.utils.ExecutorUtils;
 
 import java.util.HashSet;
 import java.util.Locale;
+import java.util.Set;
 
 /**
- * Unit tests for {@link com.android.inputmethod.latin.UserDictionaryLookup}.
+ * Unit tests for {@link PersonalDictionaryLookup}.
  *
- * Note, this test doesn't mock out the ContentResolver, in order to make sure UserDictionaryLookup
- * works in a real setting.
+ * Note, this test doesn't mock out the ContentResolver, in order to make sure
+ * {@link PersonalDictionaryLookup} works in a real setting.
  */
 @SmallTest
-public class UserDictionaryLookupTest extends AndroidTestCase {
-    private static final String TAG = UserDictionaryLookupTest.class.getSimpleName();
+public class PersonalDictionaryLookupTest extends AndroidTestCase {
+    private static final String TAG = PersonalDictionaryLookupTest.class.getSimpleName();
 
     private ContentResolver mContentResolver;
     private HashSet<Uri> mAddedBackup;
@@ -64,7 +71,7 @@
     }
 
     /**
-     * Adds the given word to UserDictionary.
+     * Adds the given word to the personal dictionary.
      *
      * @param word the word to add
      * @param locale the locale of the word to add
@@ -94,25 +101,43 @@
     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.
+        // Remove the word from the personal dictionary.
         mContentResolver.delete(uri, null, null);
     }
 
-    private UserDictionaryLookup setUpShortcut(final Locale locale) {
-        // Insert "shortcut" => "Expansion" in the UserDictionary for the given locale.
+    private PersonalDictionaryLookup setUpWord(final Locale locale) {
+        // Insert "foo" in the personal dictionary for the given locale.
+        addWord("foo", locale, 17, null);
+
+        // Create the PersonalDictionaryLookup and wait until it's loaded.
+        PersonalDictionaryLookup lookup =
+                new PersonalDictionaryLookup(mContext, ExecutorUtils.SPELLING);
+        lookup.open();
+        return lookup;
+    }
+
+    private PersonalDictionaryLookup setUpShortcut(final Locale locale) {
+        // Insert "shortcut" => "Expansion" in the personal dictionary for the given locale.
         addWord("Expansion", locale, 17, "shortcut");
 
-        // Create the UserDictionaryLookup and wait until it's loaded.
-        UserDictionaryLookup lookup = new UserDictionaryLookup(mContext, ExecutorUtils.SPELLING);
+        // Create the PersonalDictionaryLookup and wait until it's loaded.
+        PersonalDictionaryLookup lookup =
+                new PersonalDictionaryLookup(mContext, ExecutorUtils.SPELLING);
         lookup.open();
-        while (!lookup.isLoaded()) {
-        }
         return lookup;
     }
 
+    private void verifyWordExists(final Set<String> set, final String word) {
+        assertTrue(set.contains(word));
+    }
+
+    private void verifyWordDoesNotExist(final Set<String> set, final String word) {
+        assertFalse(set.contains(word));
+    }
+
     public void testShortcutKeyMatching() {
         Log.d(TAG, "testShortcutKeyMatching");
-        UserDictionaryLookup lookup = setUpShortcut(Locale.US);
+        PersonalDictionaryLookup lookup = setUpShortcut(Locale.US);
 
         assertEquals("Expansion", lookup.expandShortcut("shortcut", Locale.US));
         assertNull(lookup.expandShortcut("Shortcut", Locale.US));
@@ -125,7 +150,13 @@
 
     public void testShortcutMatchesInputCountry() {
         Log.d(TAG, "testShortcutMatchesInputCountry");
-        UserDictionaryLookup lookup = setUpShortcut(Locale.US);
+        PersonalDictionaryLookup lookup = setUpShortcut(Locale.US);
+
+        verifyWordExists(lookup.getShortcutsForLocale(Locale.US), "shortcut");
+        assertTrue(lookup.getShortcutsForLocale(Locale.UK).isEmpty());
+        assertTrue(lookup.getShortcutsForLocale(Locale.ENGLISH).isEmpty());
+        assertTrue(lookup.getShortcutsForLocale(Locale.FRENCH).isEmpty());
+        assertTrue(lookup.getShortcutsForLocale(ANY_LOCALE).isEmpty());
 
         assertEquals("Expansion", lookup.expandShortcut("shortcut", Locale.US));
         assertNull(lookup.expandShortcut("shortcut", Locale.UK));
@@ -138,7 +169,13 @@
 
     public void testShortcutMatchesInputLanguage() {
         Log.d(TAG, "testShortcutMatchesInputLanguage");
-        UserDictionaryLookup lookup = setUpShortcut(Locale.ENGLISH);
+        PersonalDictionaryLookup lookup = setUpShortcut(Locale.ENGLISH);
+
+        verifyWordExists(lookup.getShortcutsForLocale(Locale.US), "shortcut");
+        verifyWordExists(lookup.getShortcutsForLocale(Locale.UK), "shortcut");
+        verifyWordExists(lookup.getShortcutsForLocale(Locale.ENGLISH), "shortcut");
+        assertTrue(lookup.getShortcutsForLocale(Locale.FRENCH).isEmpty());
+        assertTrue(lookup.getShortcutsForLocale(ANY_LOCALE).isEmpty());
 
         assertEquals("Expansion", lookup.expandShortcut("shortcut", Locale.US));
         assertEquals("Expansion", lookup.expandShortcut("shortcut", Locale.UK));
@@ -150,7 +187,13 @@
     }
 
     public void testShortcutMatchesAnyLocale() {
-        UserDictionaryLookup lookup = setUpShortcut(UserDictionaryLookup.ANY_LOCALE);
+        PersonalDictionaryLookup lookup = setUpShortcut(PersonalDictionaryLookup.ANY_LOCALE);
+
+        verifyWordExists(lookup.getShortcutsForLocale(Locale.US), "shortcut");
+        verifyWordExists(lookup.getShortcutsForLocale(Locale.UK), "shortcut");
+        verifyWordExists(lookup.getShortcutsForLocale(Locale.ENGLISH), "shortcut");
+        verifyWordExists(lookup.getShortcutsForLocale(Locale.FRENCH), "shortcut");
+        verifyWordExists(lookup.getShortcutsForLocale(ANY_LOCALE), "shortcut");
 
         assertEquals("Expansion", lookup.expandShortcut("shortcut", Locale.US));
         assertEquals("Expansion", lookup.expandShortcut("shortcut", Locale.UK));
@@ -163,15 +206,13 @@
 
     public void testExactLocaleMatch() {
         Log.d(TAG, "testExactLocaleMatch");
+        PersonalDictionaryLookup lookup = setUpWord(Locale.US);
 
-        // Insert "Foo" as capitalized in the UserDictionary under en_US locale.
-        addWord("Foo", Locale.US, 17, null);
-
-        // Create the UserDictionaryLookup and wait until it's loaded.
-        UserDictionaryLookup lookup = new UserDictionaryLookup(mContext, ExecutorUtils.SPELLING);
-        lookup.open();
-        while (!lookup.isLoaded()) {
-        }
+        verifyWordExists(lookup.getWordsForLocale(Locale.US), "foo");
+        verifyWordDoesNotExist(lookup.getWordsForLocale(Locale.UK), "foo");
+        verifyWordDoesNotExist(lookup.getWordsForLocale(Locale.ENGLISH), "foo");
+        verifyWordDoesNotExist(lookup.getWordsForLocale(Locale.FRENCH), "foo");
+        verifyWordDoesNotExist(lookup.getWordsForLocale(ANY_LOCALE), "foo");
 
         // Any capitalization variation should match.
         assertTrue(lookup.isValidWord("foo", Locale.US));
@@ -192,15 +233,13 @@
 
     public void testSubLocaleMatch() {
         Log.d(TAG, "testSubLocaleMatch");
+        PersonalDictionaryLookup lookup = setUpWord(Locale.ENGLISH);
 
-        // Insert "Foo" as capitalized in the UserDictionary under the en locale.
-        addWord("Foo", Locale.ENGLISH, 17, null);
-
-        // Create the UserDictionaryLookup and wait until it's loaded.
-        UserDictionaryLookup lookup = new UserDictionaryLookup(mContext, ExecutorUtils.SPELLING);
-        lookup.open();
-        while (!lookup.isLoaded()) {
-        }
+        verifyWordExists(lookup.getWordsForLocale(Locale.US), "foo");
+        verifyWordExists(lookup.getWordsForLocale(Locale.UK), "foo");
+        verifyWordExists(lookup.getWordsForLocale(Locale.ENGLISH), "foo");
+        verifyWordDoesNotExist(lookup.getWordsForLocale(Locale.FRENCH), "foo");
+        verifyWordDoesNotExist(lookup.getWordsForLocale(ANY_LOCALE), "foo");
 
         // Any capitalization variation should match for both en and en_US.
         assertTrue(lookup.isValidWord("foo", Locale.ENGLISH));
@@ -217,15 +256,13 @@
 
     public void testAllLocalesMatch() {
         Log.d(TAG, "testAllLocalesMatch");
+        PersonalDictionaryLookup lookup = setUpWord(null);
 
-        // Insert "Foo" as capitalized in the UserDictionary under the all locales.
-        addWord("Foo", null, 17, null);
-
-        // Create the UserDictionaryLookup and wait until it's loaded.
-        UserDictionaryLookup lookup = new UserDictionaryLookup(mContext, ExecutorUtils.SPELLING);
-        lookup.open();
-        while (!lookup.isLoaded()) {
-        }
+        verifyWordExists(lookup.getWordsForLocale(Locale.US), "foo");
+        verifyWordExists(lookup.getWordsForLocale(Locale.UK), "foo");
+        verifyWordExists(lookup.getWordsForLocale(Locale.ENGLISH), "foo");
+        verifyWordExists(lookup.getWordsForLocale(Locale.FRENCH), "foo");
+        verifyWordExists(lookup.getWordsForLocale(ANY_LOCALE), "foo");
 
         // Any capitalization variation should match for fr, en and en_US.
         assertTrue(lookup.isValidWord("foo", ANY_LOCALE));
@@ -245,17 +282,15 @@
     public void testMultipleLocalesMatch() {
         Log.d(TAG, "testMultipleLocalesMatch");
 
-        // Insert "Foo" as capitalized in the UserDictionary under the en_US and en_CA and fr
+        // Insert "Foo" as capitalized in the personal dictionary under the en_US and en_CA and fr
         // locales.
         addWord("Foo", Locale.US, 17, null);
         addWord("foO", Locale.CANADA, 17, null);
         addWord("fOo", Locale.FRENCH, 17, null);
 
-        // Create the UserDictionaryLookup and wait until it's loaded.
-        UserDictionaryLookup lookup = new UserDictionaryLookup(mContext, ExecutorUtils.SPELLING);
+        // Create the PersonalDictionaryLookup and wait until it's loaded.
+        PersonalDictionaryLookup lookup = new PersonalDictionaryLookup(mContext, ExecutorUtils.SPELLING);
         lookup.open();
-        while (!lookup.isLoaded()) {
-        }
 
         // Both en_CA and en_US match.
         assertTrue(lookup.isValidWord("foo", Locale.CANADA));
@@ -269,17 +304,40 @@
         lookup.close();
     }
 
+    public void testManageListeners() {
+        Log.d(TAG, "testManageListeners");
+
+        PersonalDictionaryLookup lookup =
+                new PersonalDictionaryLookup(mContext, ExecutorUtils.SPELLING);
+
+        PersonalDictionaryListener listener = mock(PersonalDictionaryListener.class);
+        // Add the same listener a bunch of times. It doesn't make a difference.
+        lookup.addListener(listener);
+        lookup.addListener(listener);
+        lookup.addListener(listener);
+        lookup.notifyListeners();
+
+        verify(listener, times(1)).onUpdate();
+
+        // Remove the same listener a bunch of times. It doesn't make a difference.
+        lookup.removeListener(listener);
+        lookup.removeListener(listener);
+        lookup.removeListener(listener);
+        lookup.notifyListeners();
+
+        verifyNoMoreInteractions(listener);
+    }
+
     public void testReload() {
         Log.d(TAG, "testReload");
 
         // Insert "foo".
         Uri uri = addWord("foo", Locale.US, 17, null);
 
-        // Create the UserDictionaryLookup and wait until it's loaded.
-        UserDictionaryLookup lookup = new UserDictionaryLookup(mContext, ExecutorUtils.SPELLING);
+        // Create the PersonalDictionaryLookup and wait until it's loaded.
+        PersonalDictionaryLookup lookup =
+                new PersonalDictionaryLookup(mContext, ExecutorUtils.SPELLING);
         lookup.open();
-        while (!lookup.isLoaded()) {
-        }
 
         // "foo" should match.
         assertTrue(lookup.isValidWord("foo", Locale.US));
@@ -292,9 +350,9 @@
         addWord("bar", Locale.US, 18, null);
 
         // Wait a little bit before expecting a change. The time we wait should be greater than
-        // UserDictionaryLookup.RELOAD_DELAY_MS.
+        // PersonalDictionaryLookup.RELOAD_DELAY_MS.
         try {
-            Thread.sleep(UserDictionaryLookup.RELOAD_DELAY_MS + 1000);
+            Thread.sleep(PersonalDictionaryLookup.RELOAD_DELAY_MS + 1000);
         } catch (InterruptedException e) {
         }
 
@@ -316,11 +374,10 @@
         Uri uri = addWord("foo", Locale.GERMANY, 17, "f");
         addWord("bar", Locale.GERMANY, 17, null);
 
-        // Create the UserDictionaryLookup and wait until it's loaded.
-        UserDictionaryLookup lookup = new UserDictionaryLookup(mContext, ExecutorUtils.SPELLING);
+        // Create the PersonalDictionaryLookup and wait until it's loaded.
+        PersonalDictionaryLookup lookup =
+                new PersonalDictionaryLookup(mContext, ExecutorUtils.SPELLING);
         lookup.open();
-        while (!lookup.isLoaded()) {
-        }
 
         // "foo" should match.
         assertTrue(lookup.isValidWord("foo", Locale.GERMANY));
@@ -335,9 +392,9 @@
         deleteWord(uri);
 
         // Wait a little bit before expecting a change. The time we wait should be greater than
-        // UserDictionaryLookup.RELOAD_DELAY_MS.
+        // PersonalDictionaryLookup.RELOAD_DELAY_MS.
         try {
-            Thread.sleep(UserDictionaryLookup.RELOAD_DELAY_MS + 1000);
+            Thread.sleep(PersonalDictionaryLookup.RELOAD_DELAY_MS + 1000);
         } catch (InterruptedException e) {
         }
 
@@ -361,11 +418,10 @@
         // Insert "foo".
         Uri uri = addWord("foo", Locale.US, 17, null);
 
-        // Create the UserDictionaryLookup and wait until it's loaded.
-        UserDictionaryLookup lookup = new UserDictionaryLookup(mContext, ExecutorUtils.SPELLING);
+        // Create the PersonalDictionaryLookup and wait until it's loaded.
+        PersonalDictionaryLookup lookup =
+                new PersonalDictionaryLookup(mContext, ExecutorUtils.SPELLING);
         lookup.open();
-        while (!lookup.isLoaded()) {
-        }
 
         // "foo" should match.
         assertTrue(lookup.isValidWord("foo", Locale.US));
@@ -381,9 +437,9 @@
         addWord("bar", Locale.US, 18, null);
 
         // Wait a little bit before expecting a change. The time we wait should be greater than
-        // UserDictionaryLookup.RELOAD_DELAY_MS.
+        // PersonalDictionaryLookup.RELOAD_DELAY_MS.
         try {
-            Thread.sleep(UserDictionaryLookup.RELOAD_DELAY_MS + 1000);
+            Thread.sleep(PersonalDictionaryLookup.RELOAD_DELAY_MS + 1000);
         } catch (InterruptedException e) {
         }
 
diff --git a/tests/src/com/android/inputmethod/latin/settings/AccountsSettingsFragmentTests.java b/tests/src/com/android/inputmethod/latin/settings/AccountsSettingsFragmentTests.java
index 7a019c3..f2d8973 100644
--- a/tests/src/com/android/inputmethod/latin/settings/AccountsSettingsFragmentTests.java
+++ b/tests/src/com/android/inputmethod/latin/settings/AccountsSettingsFragmentTests.java
@@ -17,7 +17,6 @@
 package com.android.inputmethod.latin.settings;
 
 import static org.mockito.Matchers.any;
-import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import android.app.AlertDialog;
diff --git a/tests/src/com/android/inputmethod/latin/utils/CollectionUtilsTests.java b/tests/src/com/android/inputmethod/latin/utils/CollectionUtilsTests.java
index 6871a41..0cbb02c 100644
--- a/tests/src/com/android/inputmethod/latin/utils/CollectionUtilsTests.java
+++ b/tests/src/com/android/inputmethod/latin/utils/CollectionUtilsTests.java
@@ -68,7 +68,7 @@
      */
     public void testArrayAsList() {
         final ArrayList<String> empty = new ArrayList<>();
-        assertEquals(empty, CollectionUtils.arrayAsList(new String[] { }, 0, 0));
+        assertEquals(empty, CollectionUtils.arrayAsList(new String[] {}, 0, 0));
         final String[] array = { "0", "1", "2", "3", "4" };
         assertEquals(empty, CollectionUtils.arrayAsList(array, 0, 0));
         assertEquals(empty, CollectionUtils.arrayAsList(array, 1, 1));
@@ -82,12 +82,10 @@
      * results for a few cases.
      */
     public void testIsNullOrEmpty() {
-        assertTrue(CollectionUtils.isNullOrEmpty((List) null));
-        assertTrue(CollectionUtils.isNullOrEmpty((Map) null));
-        assertTrue(CollectionUtils.isNullOrEmpty(new ArrayList()));
-        assertTrue(CollectionUtils.isNullOrEmpty(new HashMap()));
-        assertTrue(CollectionUtils.isNullOrEmpty(Collections.EMPTY_LIST));
-        assertTrue(CollectionUtils.isNullOrEmpty(Collections.EMPTY_MAP));
+        assertTrue(CollectionUtils.isNullOrEmpty((List<String>) null));
+        assertTrue(CollectionUtils.isNullOrEmpty((Map<String, String>) null));
+        assertTrue(CollectionUtils.isNullOrEmpty(new ArrayList<String>()));
+        assertTrue(CollectionUtils.isNullOrEmpty(new HashMap<String, String>()));
         assertFalse(CollectionUtils.isNullOrEmpty(Collections.singletonList("Not empty")));
         assertFalse(CollectionUtils.isNullOrEmpty(Collections.singletonMap("Not", "empty")));
     }