Merge "Declare that LatinIME does not use cleartext network traffic."
diff --git a/common/src/com/android/inputmethod/latin/common/CollectionUtils.java b/common/src/com/android/inputmethod/latin/common/CollectionUtils.java
index 48df413..80fae5f 100644
--- a/common/src/com/android/inputmethod/latin/common/CollectionUtils.java
+++ b/common/src/com/android/inputmethod/latin/common/CollectionUtils.java
@@ -20,6 +20,7 @@
 
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Map;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -60,7 +61,17 @@
      * @return Whether c contains no elements.
      */
     @UsedForTesting
-    public static boolean isNullOrEmpty(@Nullable final Collection<?> c) {
+    public static boolean isNullOrEmpty(@Nullable final Collection c) {
         return c == null || c.isEmpty();
     }
+
+    /**
+     * Tests whether map contains no elements, true if map is null or map is empty.
+     * @param map Map to test.
+     * @return Whether map contains no elements.
+     */
+    @UsedForTesting
+    public static boolean isNullOrEmpty(@Nullable final Map map) {
+        return map == null || map.isEmpty();
+    }
 }
diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml
index 4973a99..28dabf6 100644
--- a/java/res/values/strings.xml
+++ b/java/res/values/strings.xml
@@ -129,6 +129,9 @@
     <!-- Description for option to enable auto capitalization of sentences -->
     <string name="auto_cap_summary">Capitalize the first word of each sentence</string>
 
+    <!-- Option to edit personal dictionary. [CHAR_LIMIT=30]-->
+    <string name="edit_personal_dictionary">Personal dictionary</string>
+
     <!-- Option to configure dictionaries -->
     <string name="configure_dictionaries_title">Add-on dictionaries</string>
     <!-- Name of the main dictionary, as opposed to auxiliary dictionaries (medical/entertainment/sports...) -->
diff --git a/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java b/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java
index a2789cc..fbc8991 100644
--- a/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java
+++ b/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java
@@ -34,6 +34,8 @@
 import java.util.List;
 import java.util.TreeMap;
 
+import javax.annotation.Nullable;
+
 /**
  * Various helper functions for the state database
  */
@@ -705,6 +707,7 @@
      * @param version the word list version.
      * @return the metadata about this word list.
      */
+    @Nullable
     public static ContentValues getContentValuesByWordListId(final SQLiteDatabase db,
             final String id, final int version) {
         final Cursor cursor = db.query(METADATA_TABLE_NAME,
diff --git a/java/src/com/android/inputmethod/dictionarypack/MetadataHandler.java b/java/src/com/android/inputmethod/dictionarypack/MetadataHandler.java
index 329b9f6..e5d632f 100644
--- a/java/src/com/android/inputmethod/dictionarypack/MetadataHandler.java
+++ b/java/src/com/android/inputmethod/dictionarypack/MetadataHandler.java
@@ -19,6 +19,7 @@
 import android.content.ContentValues;
 import android.content.Context;
 import android.database.Cursor;
+import android.util.Log;
 
 import java.io.IOException;
 import java.io.InputStreamReader;
@@ -30,10 +31,13 @@
  * Helper class to easy up manipulation of dictionary pack metadata.
  */
 public class MetadataHandler {
+
+    public static final String TAG = MetadataHandler.class.getSimpleName();
+
     // The canonical file name for metadata. This is not the name of a real file on the
     // device, but a symbolic name used in the database and in metadata handling. It is never
     // tested against, only used for human-readability as the file name for the metadata.
-    public final static String METADATA_FILENAME = "metadata.json";
+    public static final String METADATA_FILENAME = "metadata.json";
 
     /**
      * Reads the data from the cursor and store it in metadata objects.
@@ -114,6 +118,14 @@
             final String clientId, final String wordListId, final int version) {
         final ContentValues contentValues = MetadataDbHelper.getContentValuesByWordListId(
                 MetadataDbHelper.getDb(context, clientId), wordListId, version);
+        if (contentValues == null) {
+            // TODO: Figure out why this would happen.
+            // Check if this happens when the metadata gets updated in the background.
+            Log.e(TAG, String.format( "Unable to find the current metadata for wordlist "
+                            + "(clientId=%s, wordListId=%s, version=%d) on the database",
+                    clientId, wordListId, version));
+            return null;
+        }
         return WordListMetadata.createFromContentValues(contentValues);
     }
 
diff --git a/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java b/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java
index 30ff0b8..e720f3c 100644
--- a/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java
+++ b/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java
@@ -1143,6 +1143,9 @@
             }
             final WordListMetadata wordListMetaData = MetadataHandler.getCurrentMetadataForWordList(
                     context, clientId, wordlistId, version);
+            if (wordListMetaData == null) {
+                return;
+            }
 
             final ActionBatch actions = new ActionBatch();
             actions.add(new ActionBatch.StartDownloadAction(
diff --git a/java/src/com/android/inputmethod/dictionarypack/WordListMetadata.java b/java/src/com/android/inputmethod/dictionarypack/WordListMetadata.java
index 59f75e4..99cffb8 100644
--- a/java/src/com/android/inputmethod/dictionarypack/WordListMetadata.java
+++ b/java/src/com/android/inputmethod/dictionarypack/WordListMetadata.java
@@ -18,6 +18,8 @@
 
 import android.content.ContentValues;
 
+import javax.annotation.Nonnull;
+
 /**
  * The metadata for a single word list.
  *
@@ -77,7 +79,7 @@
      *
      * If this lacks any required field, IllegalArgumentException is thrown.
      */
-    public static WordListMetadata createFromContentValues(final ContentValues values) {
+    public static WordListMetadata createFromContentValues(@Nonnull final ContentValues values) {
         final String id = values.getAsString(MetadataDbHelper.WORDLISTID_COLUMN);
         final Integer type = values.getAsInteger(MetadataDbHelper.TYPE_COLUMN);
         final String description = values.getAsString(MetadataDbHelper.DESCRIPTION_COLUMN);
diff --git a/java/src/com/android/inputmethod/latin/Dictionary.java b/java/src/com/android/inputmethod/latin/Dictionary.java
index 16dcb32..e00219c 100644
--- a/java/src/com/android/inputmethod/latin/Dictionary.java
+++ b/java/src/com/android/inputmethod/latin/Dictionary.java
@@ -39,6 +39,10 @@
     public static final String TYPE_USER_TYPED = "user_typed";
     public static final PhonyDictionary DICTIONARY_USER_TYPED = new PhonyDictionary(TYPE_USER_TYPED);
 
+    public static final String TYPE_USER_SHORTCUT = "user_shortcut";
+    public static final PhonyDictionary DICTIONARY_USER_SHORTCUT =
+            new PhonyDictionary(TYPE_USER_SHORTCUT);
+
     public static final String TYPE_APPLICATION_DEFINED = "application_defined";
     public static final PhonyDictionary DICTIONARY_APPLICATION_DEFINED =
             new PhonyDictionary(TYPE_APPLICATION_DEFINED);
diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
index 37899d2..9070957 100644
--- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
@@ -650,7 +650,8 @@
         reloadDictionaryIfRequired();
         final String dictName = mDictName;
         final File dictFile = mDictFile;
-        final AsyncResultHolder<DictionaryStats> result = new AsyncResultHolder<>();
+        final AsyncResultHolder<DictionaryStats> result =
+                new AsyncResultHolder<>("DictionaryStats");
         asyncExecuteTaskWithLock(mLock.readLock(), new Runnable() {
             @Override
             public void run() {
@@ -724,7 +725,8 @@
      */
     public WordProperty[] getWordPropertiesForSyncing() {
         reloadDictionaryIfRequired();
-        final AsyncResultHolder<WordProperty[]> result = new AsyncResultHolder<>();
+        final AsyncResultHolder<WordProperty[]> result =
+                new AsyncResultHolder<>("WordPropertiesForSync");
         asyncExecuteTaskWithLock(mLock.readLock(), new Runnable() {
             @Override
             public void run() {
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/UserDictionaryLookup.java b/java/src/com/android/inputmethod/latin/UserDictionaryLookup.java
similarity index 66%
rename from java/src/com/android/inputmethod/latin/spellcheck/UserDictionaryLookup.java
rename to java/src/com/android/inputmethod/latin/UserDictionaryLookup.java
index f2491f4..2569723 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/UserDictionaryLookup.java
+++ b/java/src/com/android/inputmethod/latin/UserDictionaryLookup.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.inputmethod.latin.spellcheck;
+package com.android.inputmethod.latin;
 
 import android.content.ContentResolver;
 import android.content.Context;
@@ -22,9 +22,11 @@
 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;
 
@@ -36,6 +38,9 @@
 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".
  *
@@ -47,7 +52,6 @@
  * 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
@@ -79,7 +83,12 @@
     @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().
@@ -88,12 +97,11 @@
         @Override
         public void run() {
             if (DEBUG) {
-                Log.d(TAG, "Executing (re)load");
+                Log.d(mTag, "Executing (re)load");
             }
             loadUserDictionary();
         }
     }
-    private final UserDictionaryLoader mLoader = new UserDictionaryLoader();
 
     /**
      *  Content observer for UserDictionary changes.  It has the following properties:
@@ -122,7 +130,7 @@
         @Override
         public void onChange(boolean selfChange, Uri uri) {
             if (DEBUG) {
-                Log.d(TAG, "Received content observer onChange notification for URI: " + uri);
+                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() &&
@@ -131,20 +139,20 @@
                 boolean isCancelled = mReloadFuture.cancel(false);
                 if (DEBUG) {
                     if (isCancelled) {
-                        Log.d(TAG, "Successfully canceled previous reload request");
+                        Log.d(mTag, "Successfully canceled previous reload request");
                     } else {
-                        Log.d(TAG, "Unable to cancel previous reload request");
+                        Log.d(mTag, "Unable to cancel previous reload request");
                     }
                 }
             }
 
             if (DEBUG) {
-                Log.d(TAG, "Scheduling reload in " + RELOAD_DELAY_MS + " ms");
+                Log.d(mTag, "Scheduling reload in " + RELOAD_DELAY_MS + " ms");
             }
 
             // Schedule a new reload after RELOAD_DELAY_MS.
-            mReloadFuture = ExecutorUtils.getBackgroundExecutor(ExecutorUtils.SPELLING)
-                    .schedule(mLoader, RELOAD_DELAY_MS, TimeUnit.MILLISECONDS);
+            mReloadFuture = ExecutorUtils.getBackgroundExecutor(mServiceName)
+                    .schedule(new UserDictionaryLoader(), RELOAD_DELAY_MS, TimeUnit.MILLISECONDS);
         }
     }
     private final ContentObserver mObserver = new UserDictionaryContentObserver();
@@ -167,6 +175,12 @@
     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.
      */
@@ -175,18 +189,24 @@
     /**
      * @param context the context from which to obtain content resolver
      */
-    public UserDictionaryLookup(Context context) {
-        if (DEBUG) {
-            Log.d(TAG, "UserDictionaryLookup constructor with context: " + context);
-        }
+    public UserDictionaryLookup(@Nonnull final Context context, @Nonnull final String serviceName) {
+        mTag = serviceName + ".UserDictionaryLookup";
+
+        Log.i(mTag, "create()");
+
+        mServiceName = serviceName;
 
         // Obtain a content resolver.
         mResolver = context.getContentResolver();
+    }
+
+    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.
-        ExecutorUtils.getBackgroundExecutor(ExecutorUtils.SPELLING).execute(mLoader);
+        loadUserDictionary();
 
         // Register the observer to be notified on changes to the UserDictionary and all individual
         // items.
@@ -210,7 +230,7 @@
     public void finalize() throws Throwable {
         try {
             if (DEBUG) {
-                Log.d(TAG, "Finalize called, calling close()");
+                Log.d(mTag, "Finalize called, calling close()");
             }
             close();
         } finally {
@@ -227,7 +247,7 @@
     @Override
     public void close() {
         if (DEBUG) {
-            Log.d(TAG, "Close called (no pun intended), cleaning up executor and observer");
+            Log.d(mTag, "Close called (no pun intended), cleaning up executor and observer");
         }
         if (mIsClosed.compareAndSet(false, true)) {
             // Unregister the content observer.
@@ -240,9 +260,8 @@
      *
      * @return true if the initial load is successful
      */
-    @UsedForTesting
-    boolean isLoaded() {
-        return mDictWords != null;
+    public boolean isLoaded() {
+        return mDictWords != null && mShortcutsPerLocale != null;
     }
 
     /**
@@ -255,14 +274,13 @@
      * @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) {
+    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(TAG, "isValidWord invoked, but initial load not complete");
+                Log.d(mTag, "isValidWord invoked, but initial load not complete");
             }
             return false;
         }
@@ -271,7 +289,7 @@
         final HashMap<String, ArrayList<Locale>> dictWords = mDictWords;
 
         if (DEBUG) {
-            Log.d(TAG, "isValidWord invoked for word [" + word +
+            Log.d(mTag, "isValidWord invoked for word [" + word +
                     "] in locale " + locale);
         }
         // Lowercase the word using the given locale. Note, that dictionary
@@ -282,13 +300,13 @@
         final ArrayList<Locale> dictLocales = dictWords.get(lowercased);
         if (null == dictLocales) {
             if (DEBUG) {
-                Log.d(TAG, "isValidWord=false, since there is no entry for " +
+                Log.d(mTag, "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 +
+                Log.d(mTag, "isValidWord found an entry for lowercased word [" + lowercased +
                         "]; examining locales");
             }
             // Iterate over the locales this word is in.
@@ -296,28 +314,90 @@
                 final int matchLevel = LocaleUtils.getMatchLevel(dictLocale.toString(),
                         locale.toString());
                 if (DEBUG) {
-                    Log.d(TAG, "matchLevel for dictLocale=" + dictLocale + ", locale=" +
+                    Log.d(mTag, "matchLevel for dictLocale=" + dictLocale + ", locale=" +
                             locale + " is " + matchLevel);
                 }
                 if (LocaleUtils.isMatch(matchLevel)) {
                     if (DEBUG) {
-                        Log.d(TAG, "isValidWord=true, since matchLevel " + matchLevel +
+                        Log.d(mTag, "isValidWord=true, since matchLevel " + matchLevel +
                                 " is a match");
                     }
                     return true;
                 }
                 if (DEBUG) {
-                    Log.d(TAG, "matchLevel " + matchLevel + " is not a match");
+                    Log.d(mTag, "matchLevel " + matchLevel + " is not a match");
                 }
             }
             if (DEBUG) {
-                Log.d(TAG, "isValidWord=false, since none of the locales matched");
+                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.
@@ -325,45 +405,36 @@
     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");
-            }
+            Log.i(mTag, "loadUserDictionary() : Already Loading (exit)");
             return;
         }
-        if (DEBUG) {
-            Log.d(TAG, "Loading UserDictionary");
-        }
+        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) {
-            if (DEBUG) {
-                Log.d(TAG, "No entries found in UserDictionary");
-            }
+            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);
+                final int dictLocaleIndex = cursor.getColumnIndex(UserDictionary.Words.LOCALE);
                 if (dictLocaleIndex < 0) {
                     if (DEBUG) {
-                        Log.d(TAG, "Encountered UserDictionary entry " +
-                                "without LOCALE, skipping");
+                        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);
+                final int dictWordIndex = cursor.getColumnIndex(UserDictionary.Words.WORD);
                 if (dictWordIndex < 0) {
                     if (DEBUG) {
-                        Log.d(TAG, "Encountered UserDictionary entry without " +
-                                "WORD, skipping");
+                        Log.d(mTag, "Encountered UserDictionary entry without WORD, skipping");
                     }
                     continue;
                 }
@@ -371,7 +442,7 @@
                 final String rawDictWord = cursor.getString(dictWordIndex);
                 if (null == rawDictWord) {
                     if (DEBUG) {
-                        Log.d(TAG, "Encountered null word");
+                        Log.d(mTag, "Encountered null word");
                     }
                     continue;
                 }
@@ -380,19 +451,17 @@
                 String localeString = cursor.getString(dictLocaleIndex);
                 if (null == localeString) {
                     if (DEBUG) {
-                        Log.d(TAG, "Encountered null locale for word [" +
+                        Log.d(mTag, "Encountered null locale for word [" +
                                 rawDictWord + "], assuming all locales");
                     }
-                    // For purposes of LocaleUtils, an empty locale matches
-                    // everything.
+                    // For purposes of LocaleUtils, an empty locale matches everything.
                     localeString = "";
                 }
-                final Locale dictLocale = LocaleUtils.constructLocaleFromString(
-                        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 +
+                    Log.d(mTag, "Incorporating UserDictionary word [" + dictWord +
                             "] for locale " + dictLocale);
                 }
                 // Check if there is an existing entry for this word.
@@ -400,7 +469,7 @@
                 if (null == dictLocales) {
                     // If there is no entry for this word, create one.
                     if (DEBUG) {
-                        Log.d(TAG, "Word [" + dictWord +
+                        Log.d(mTag, "Word [" + dictWord +
                                 "] not seen for other locales, creating new entry");
                     }
                     dictLocales = new ArrayList<>();
@@ -408,13 +477,42 @@
                 }
                 // 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);
             }
         }
 
-        // Atomically replace the copy of mDictWords.
+        // 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");
     }
 }
diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
index cf4064b..9ceb371 100644
--- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
+++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
@@ -104,6 +104,10 @@
     private boolean mIsAutoCorrectionIndicatorOn;
     private long mDoubleSpacePeriodCountdownStart;
 
+    // The word being corrected while the cursor is in the middle of the word.
+    // Note: This does not have a composing span, so it must be handled separately.
+    private String mWordBeingCorrectedByCursor = null;
+
     /**
      * Create a new instance of the input logic.
      * @param latinIME the instance of the parent LatinIME. We should remove this when we can.
@@ -133,6 +137,7 @@
      */
     public void startInput(final String combiningSpec, final SettingsValues settingsValues) {
         mEnteredText = null;
+        mWordBeingCorrectedByCursor = null;
         if (!mWordComposer.getTypedWord().isEmpty()) {
             // For messaging apps that offer send button, the IME does not get the opportunity
             // to capture the last word. This block should capture those uncommitted words.
@@ -247,6 +252,7 @@
         // Space state must be updated before calling updateShiftState
         mSpaceState = SpaceState.NONE;
         mEnteredText = text;
+        mWordBeingCorrectedByCursor = null;
         inputTransaction.setDidAffectContents();
         inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW);
         return inputTransaction;
@@ -386,6 +392,15 @@
             // it here, which means we'll keep outdated suggestions for a split second but the
             // visual result is better.
             resetEntireInputState(newSelStart, newSelEnd, false /* clearSuggestionStrip */);
+            // If the user is in the middle of correcting a word, we should learn it before moving
+            // the cursor away.
+            if (!TextUtils.isEmpty(mWordBeingCorrectedByCursor)) {
+                final int timeStampInSeconds = (int)TimeUnit.MILLISECONDS.toSeconds(
+                        System.currentTimeMillis());
+                mDictionaryFacilitator.addToUserHistory(mWordBeingCorrectedByCursor, false,
+                        NgramContext.EMPTY_PREV_WORDS_INFO, timeStampInSeconds,
+                        settingsValues.mBlockPotentiallyOffensive);
+            }
         } else {
             // resetEntireInputState calls resetCachesUponCursorMove, but forcing the
             // composition to end. But in all cases where we don't reset the entire input
@@ -401,6 +416,7 @@
         mLatinIME.mHandler.postResumeSuggestions(true /* shouldDelay */);
         // Stop the last recapitalization, if started.
         mRecapitalizeStatus.stop();
+        mWordBeingCorrectedByCursor = null;
         return true;
     }
 
@@ -420,6 +436,7 @@
     public InputTransaction onCodeInput(final SettingsValues settingsValues,
             @Nonnull final Event event, final int keyboardShiftMode,
             final int currentKeyboardScriptId, final LatinIME.UIHandler handler) {
+        mWordBeingCorrectedByCursor = null;
         final Event processedEvent = mWordComposer.processEvent(event);
         final InputTransaction inputTransaction = new InputTransaction(settingsValues,
                 processedEvent, SystemClock.uptimeMillis(), mSpaceState,
@@ -453,6 +470,14 @@
             }
             currentEvent = currentEvent.mNextEvent;
         }
+        // Try to record the word being corrected when the user enters a word character or
+        // the backspace key.
+        if (!mWordComposer.isComposingWord()
+                && (settingsValues.isWordCodePoint(processedEvent.mCodePoint) ||
+                        processedEvent.mKeyCode == Constants.CODE_DELETE)) {
+            mWordBeingCorrectedByCursor = getWordAtCursor(
+                   settingsValues, currentKeyboardScriptId);
+        }
         if (!inputTransaction.didAutoCorrect() && processedEvent.mKeyCode != Constants.CODE_SHIFT
                 && processedEvent.mKeyCode != Constants.CODE_CAPSLOCK
                 && processedEvent.mKeyCode != Constants.CODE_SWITCH_ALPHA_SYMBOL)
@@ -466,6 +491,7 @@
 
     public void onStartBatchInput(final SettingsValues settingsValues,
             final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler) {
+        mWordBeingCorrectedByCursor = null;
         mInputLogicHandler.onStartBatchInput();
         handler.showGesturePreviewAndSuggestionStrip(
                 SuggestedWords.getEmptyInstance(), false /* dismissGestureFloatingPreviewText */);
@@ -1151,27 +1177,30 @@
         }
     }
 
-    boolean unlearnWordBeingDeleted(
-            final SettingsValues settingsValues,final int currentKeyboardScriptId) {
-        // If we just started backspacing to delete a previous word (but have not
-        // entered the composing state yet), unlearn the word.
-        // TODO: Consider tracking whether or not this word was typed by the user.
+    String getWordAtCursor(final SettingsValues settingsValues, final int currentKeyboardScriptId) {
         if (!mConnection.hasSelection()
                 && settingsValues.isSuggestionsEnabledPerUserSettings()
-                && settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces
-                && !mConnection.isCursorFollowedByWordCharacter(
-                        settingsValues.mSpacingAndPunctuations)) {
+                && settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces) {
             final TextRange range = mConnection.getWordRangeAtCursor(
                     settingsValues.mSpacingAndPunctuations,
                     currentKeyboardScriptId);
-            if (range == null) {
-                // Happens if we don't have an input connection at all.
-                return false;
+            if (range != null) {
+                return range.mWord.toString();
             }
-            final String wordBeingDeleted = range.mWord.toString();
-            if (!wordBeingDeleted.isEmpty()) {
-                unlearnWord(wordBeingDeleted, settingsValues,
-                        Constants.EVENT_BACKSPACE);
+        }
+        return "";
+    }
+
+    boolean unlearnWordBeingDeleted(
+            final SettingsValues settingsValues, final int currentKeyboardScriptId) {
+        // If we just started backspacing to delete a previous word (but have not
+        // entered the composing state yet), unlearn the word.
+        // TODO: Consider tracking whether or not this word was typed by the user.
+        if (!mConnection.isCursorFollowedByWordCharacter(settingsValues.mSpacingAndPunctuations)) {
+            final String wordBeingDeleted = getWordAtCursor(
+                    settingsValues, currentKeyboardScriptId);
+            if (!TextUtils.isEmpty(wordBeingDeleted)) {
+                unlearnWord(wordBeingDeleted, settingsValues, Constants.EVENT_BACKSPACE);
                 return true;
             }
         }
@@ -1407,7 +1436,7 @@
             return;
         }
 
-        final AsyncResultHolder<SuggestedWords> holder = new AsyncResultHolder<>();
+        final AsyncResultHolder<SuggestedWords> holder = new AsyncResultHolder<>("Suggest");
         mInputLogicHandler.getSuggestedWords(inputStyle, SuggestedWords.NOT_A_SEQUENCE_NUMBER,
                 new OnGetSuggestedWordsCallback() {
                     @Override
@@ -2075,24 +2104,60 @@
      */
     private void commitChosenWord(final SettingsValues settingsValues, final String chosenWord,
             final int commitType, final String separatorString) {
+        long startTimeMillis = 0;
+        if (DebugFlags.DEBUG_ENABLED) {
+            startTimeMillis = System.currentTimeMillis();
+            Log.d(TAG, "commitChosenWord() : [" + chosenWord + "]");
+        }
         final SuggestedWords suggestedWords = mSuggestedWords;
         final CharSequence chosenWordWithSuggestions =
                 SuggestionSpanUtils.getTextWithSuggestionSpan(mLatinIME, chosenWord,
                         suggestedWords);
+        if (DebugFlags.DEBUG_ENABLED) {
+            long runTimeMillis = System.currentTimeMillis() - startTimeMillis;
+            Log.d(TAG, "commitChosenWord() : " + runTimeMillis + " ms to run "
+                    + "SuggestionSpanUtils.getTextWithSuggestionSpan()");
+            startTimeMillis = System.currentTimeMillis();
+        }
         // When we are composing word, get n-gram context from the 2nd previous word because the
         // 1st previous word is the word to be committed. Otherwise get n-gram context from the 1st
         // previous word.
         final NgramContext ngramContext = mConnection.getNgramContextFromNthPreviousWord(
                 settingsValues.mSpacingAndPunctuations, mWordComposer.isComposingWord() ? 2 : 1);
+        if (DebugFlags.DEBUG_ENABLED) {
+            long runTimeMillis = System.currentTimeMillis() - startTimeMillis;
+            Log.d(TAG, "commitChosenWord() : " + runTimeMillis + " ms to run "
+                    + "Connection.getNgramContextFromNthPreviousWord()");
+            Log.d(TAG, "commitChosenWord() : NgramContext = " + ngramContext);
+            startTimeMillis = System.currentTimeMillis();
+        }
         mConnection.commitText(chosenWordWithSuggestions, 1);
+        if (DebugFlags.DEBUG_ENABLED) {
+            long runTimeMillis = System.currentTimeMillis() - startTimeMillis;
+            Log.d(TAG, "commitChosenWord() : " + runTimeMillis + " ms to run "
+                    + "Connection.commitText");
+            startTimeMillis = System.currentTimeMillis();
+        }
         // Add the word to the user history dictionary
         performAdditionToUserHistoryDictionary(settingsValues, chosenWord, ngramContext);
+        if (DebugFlags.DEBUG_ENABLED) {
+            long runTimeMillis = System.currentTimeMillis() - startTimeMillis;
+            Log.d(TAG, "commitChosenWord() : " + runTimeMillis + " ms to run "
+                    + "performAdditionToUserHistoryDictionary()");
+            startTimeMillis = System.currentTimeMillis();
+        }
         // TODO: figure out here if this is an auto-correct or if the best word is actually
         // what user typed. Note: currently this is done much later in
         // LastComposedWord#didCommitTypedWord by string equality of the remembered
         // strings.
         mLastComposedWord = mWordComposer.commitWord(commitType,
                 chosenWordWithSuggestions, separatorString, ngramContext);
+        if (DebugFlags.DEBUG_ENABLED) {
+            long runTimeMillis = System.currentTimeMillis() - startTimeMillis;
+            Log.d(TAG, "commitChosenWord() : " + runTimeMillis + " ms to run "
+                    + "WordComposer.commitWord()");
+            startTimeMillis = System.currentTimeMillis();
+        }
     }
 
     /**
diff --git a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java
index d112e72..94573a6 100644
--- a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java
+++ b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java
@@ -209,7 +209,7 @@
                 prefs, DebugSettings.PREF_KEY_PREVIEW_DISMISS_END_Y_SCALE,
                 defaultKeyPreviewDismissEndScale);
         mDisplayOrientation = res.getConfiguration().orientation;
-        mAppWorkarounds = new AsyncResultHolder<>();
+        mAppWorkarounds = new AsyncResultHolder<>("AppWorkarounds");
         final PackageInfo packageInfo = TargetPackageInfoGetterTask.getCachedPackageInfo(
                 mInputAttributes.mTargetApplicationPackageName);
         if (null != packageInfo) {
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
index 766b385..4625e8e 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
@@ -24,7 +24,6 @@
 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;
@@ -83,7 +82,6 @@
 
     public static final String SINGLE_QUOTE = "\u0027";
     public static final String APOSTROPHE = "\u2019";
-    private UserDictionaryLookup mUserDictionaryLookup;
 
     public AndroidSpellCheckerService() {
         super();
@@ -95,30 +93,11 @@
     @Override
     public void onCreate() {
         super.onCreate();
-        mRecommendedThreshold =
-                Float.parseFloat(getString(R.string.spellchecker_recommended_threshold_value));
+        mRecommendedThreshold = Float.parseFloat(
+                getString(R.string.spellchecker_recommended_threshold_value));
         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() {
@@ -181,16 +160,6 @@
     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/utils/AsyncResultHolder.java b/java/src/com/android/inputmethod/latin/utils/AsyncResultHolder.java
index 952ac2a..1525f2d 100644
--- a/java/src/com/android/inputmethod/latin/utils/AsyncResultHolder.java
+++ b/java/src/com/android/inputmethod/latin/utils/AsyncResultHolder.java
@@ -16,6 +16,8 @@
 
 package com.android.inputmethod.latin.utils;
 
+import android.util.Log;
+
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
@@ -29,9 +31,11 @@
     private final Object mLock = new Object();
 
     private E mResult;
+    private final String mTag;
     private final CountDownLatch mLatch;
 
-    public AsyncResultHolder() {
+    public AsyncResultHolder(final String tag) {
+        mTag = tag;
         mLatch = new CountDownLatch(1);
     }
 
@@ -61,6 +65,7 @@
         try {
             return mLatch.await(timeOut, TimeUnit.MILLISECONDS) ? mResult : defaultValue;
         } catch (InterruptedException e) {
+            Log.w(mTag, "get() : Interrupted after " + timeOut + " ms");
             return defaultValue;
         }
     }
diff --git a/tests/src/com/android/inputmethod/latin/spellcheck/UserDictionaryLookupTest.java b/tests/src/com/android/inputmethod/latin/UserDictionaryLookupTest.java
similarity index 69%
rename from tests/src/com/android/inputmethod/latin/spellcheck/UserDictionaryLookupTest.java
rename to tests/src/com/android/inputmethod/latin/UserDictionaryLookupTest.java
index e5c8139..d8060f2 100644
--- a/tests/src/com/android/inputmethod/latin/spellcheck/UserDictionaryLookupTest.java
+++ b/tests/src/com/android/inputmethod/latin/UserDictionaryLookupTest.java
@@ -14,7 +14,9 @@
  * limitations under the License.
  */
 
-package com.android.inputmethod.latin.spellcheck;
+package com.android.inputmethod.latin;
+
+import static com.android.inputmethod.latin.UserDictionaryLookup.ANY_LOCALE;
 
 import android.annotation.SuppressLint;
 import android.content.ContentResolver;
@@ -25,11 +27,13 @@
 import android.test.suitebuilder.annotation.SmallTest;
 import android.util.Log;
 
+import com.android.inputmethod.latin.utils.ExecutorUtils;
+
 import java.util.HashSet;
 import java.util.Locale;
 
 /**
- * Unit tests for {@link UserDictionaryLookup}.
+ * Unit tests for {@link com.android.inputmethod.latin.UserDictionaryLookup}.
  *
  * Note, this test doesn't mock out the ContentResolver, in order to make sure UserDictionaryLookup
  * works in a real setting.
@@ -68,9 +72,9 @@
      * @return the Uri for the given word
      */
     @SuppressLint("NewApi")
-    private Uri addWord(final String word, final Locale locale, int frequency) {
+    private Uri addWord(final String word, final Locale locale, int frequency, String shortcut) {
         // Add the given word for the given locale.
-        UserDictionary.Words.addWord(mContext, word, frequency, null, locale);
+        UserDictionary.Words.addWord(mContext, word, frequency, shortcut, locale);
         // Obtain an Uri for the given word.
         Cursor cursor = mContentResolver.query(UserDictionary.Words.CONTENT_URI, null,
                 UserDictionary.Words.WORD + "='" + word + "'", null, null);
@@ -94,14 +98,78 @@
         mContentResolver.delete(uri, null, null);
     }
 
+    private UserDictionaryLookup setUpShortcut(final Locale locale) {
+        // Insert "shortcut" => "Expansion" in the UserDictionary 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);
+        lookup.open();
+        while (!lookup.isLoaded()) {
+        }
+        return lookup;
+    }
+
+    public void testShortcutKeyMatching() {
+        Log.d(TAG, "testShortcutKeyMatching");
+        UserDictionaryLookup lookup = setUpShortcut(Locale.US);
+
+        assertEquals("Expansion", lookup.expandShortcut("shortcut", Locale.US));
+        assertNull(lookup.expandShortcut("Shortcut", Locale.US));
+        assertNull(lookup.expandShortcut("SHORTCUT", Locale.US));
+        assertNull(lookup.expandShortcut("shortcu", Locale.US));
+        assertNull(lookup.expandShortcut("shortcutt", Locale.US));
+
+        lookup.close();
+    }
+
+    public void testShortcutMatchesInputCountry() {
+        Log.d(TAG, "testShortcutMatchesInputCountry");
+        UserDictionaryLookup lookup = setUpShortcut(Locale.US);
+
+        assertEquals("Expansion", lookup.expandShortcut("shortcut", Locale.US));
+        assertNull(lookup.expandShortcut("shortcut", Locale.UK));
+        assertNull(lookup.expandShortcut("shortcut", Locale.ENGLISH));
+        assertNull(lookup.expandShortcut("shortcut", Locale.FRENCH));
+        assertNull(lookup.expandShortcut("shortcut", ANY_LOCALE));
+
+        lookup.close();
+    }
+
+    public void testShortcutMatchesInputLanguage() {
+        Log.d(TAG, "testShortcutMatchesInputLanguage");
+        UserDictionaryLookup lookup = setUpShortcut(Locale.ENGLISH);
+
+        assertEquals("Expansion", lookup.expandShortcut("shortcut", Locale.US));
+        assertEquals("Expansion", lookup.expandShortcut("shortcut", Locale.UK));
+        assertEquals("Expansion", lookup.expandShortcut("shortcut", Locale.ENGLISH));
+        assertNull(lookup.expandShortcut("shortcut", Locale.FRENCH));
+        assertNull(lookup.expandShortcut("shortcut", ANY_LOCALE));
+
+        lookup.close();
+    }
+
+    public void testShortcutMatchesAnyLocale() {
+        UserDictionaryLookup lookup = setUpShortcut(UserDictionaryLookup.ANY_LOCALE);
+
+        assertEquals("Expansion", lookup.expandShortcut("shortcut", Locale.US));
+        assertEquals("Expansion", lookup.expandShortcut("shortcut", Locale.UK));
+        assertEquals("Expansion", lookup.expandShortcut("shortcut", Locale.ENGLISH));
+        assertEquals("Expansion", lookup.expandShortcut("shortcut", Locale.FRENCH));
+        assertEquals("Expansion", lookup.expandShortcut("shortcut", ANY_LOCALE));
+
+        lookup.close();
+    }
+
     public void testExactLocaleMatch() {
         Log.d(TAG, "testExactLocaleMatch");
 
         // Insert "Foo" as capitalized in the UserDictionary under en_US locale.
-        addWord("Foo", Locale.US, 17);
+        addWord("Foo", Locale.US, 17, null);
 
         // Create the UserDictionaryLookup and wait until it's loaded.
-        UserDictionaryLookup lookup = new UserDictionaryLookup(mContext);
+        UserDictionaryLookup lookup = new UserDictionaryLookup(mContext, ExecutorUtils.SPELLING);
+        lookup.open();
         while (!lookup.isLoaded()) {
         }
 
@@ -117,7 +185,7 @@
         assertFalse(lookup.isValidWord("foo", Locale.ENGLISH));
         assertFalse(lookup.isValidWord("foo", Locale.UK));
         assertFalse(lookup.isValidWord("foo", Locale.FRENCH));
-        assertFalse(lookup.isValidWord("foo", new Locale("")));
+        assertFalse(lookup.isValidWord("foo", ANY_LOCALE));
 
         lookup.close();
     }
@@ -126,10 +194,11 @@
         Log.d(TAG, "testSubLocaleMatch");
 
         // Insert "Foo" as capitalized in the UserDictionary under the en locale.
-        addWord("Foo", Locale.ENGLISH, 17);
+        addWord("Foo", Locale.ENGLISH, 17, null);
 
         // Create the UserDictionaryLookup and wait until it's loaded.
-        UserDictionaryLookup lookup = new UserDictionaryLookup(mContext);
+        UserDictionaryLookup lookup = new UserDictionaryLookup(mContext, ExecutorUtils.SPELLING);
+        lookup.open();
         while (!lookup.isLoaded()) {
         }
 
@@ -150,15 +219,16 @@
         Log.d(TAG, "testAllLocalesMatch");
 
         // Insert "Foo" as capitalized in the UserDictionary under the all locales.
-        addWord("Foo", null, 17);
+        addWord("Foo", null, 17, null);
 
         // Create the UserDictionaryLookup and wait until it's loaded.
-        UserDictionaryLookup lookup = new UserDictionaryLookup(mContext);
+        UserDictionaryLookup lookup = new UserDictionaryLookup(mContext, ExecutorUtils.SPELLING);
+        lookup.open();
         while (!lookup.isLoaded()) {
         }
 
         // Any capitalization variation should match for fr, en and en_US.
-        assertTrue(lookup.isValidWord("foo", new Locale("")));
+        assertTrue(lookup.isValidWord("foo", ANY_LOCALE));
         assertTrue(lookup.isValidWord("foo", Locale.FRENCH));
         assertTrue(lookup.isValidWord("foo", Locale.ENGLISH));
         assertTrue(lookup.isValidWord("foo", Locale.US));
@@ -177,12 +247,13 @@
 
         // 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);
+        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);
+        UserDictionaryLookup lookup = new UserDictionaryLookup(mContext, ExecutorUtils.SPELLING);
+        lookup.open();
         while (!lookup.isLoaded()) {
         }
 
@@ -193,7 +264,7 @@
         // 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("")));
+        assertFalse(lookup.isValidWord("foo", ANY_LOCALE));
 
         lookup.close();
     }
@@ -202,10 +273,11 @@
         Log.d(TAG, "testReload");
 
         // Insert "foo".
-        Uri uri = addWord("foo", Locale.US, 17);
+        Uri uri = addWord("foo", Locale.US, 17, null);
 
         // Create the UserDictionaryLookup and wait until it's loaded.
-        UserDictionaryLookup lookup = new UserDictionaryLookup(mContext);
+        UserDictionaryLookup lookup = new UserDictionaryLookup(mContext, ExecutorUtils.SPELLING);
+        lookup.open();
         while (!lookup.isLoaded()) {
         }
 
@@ -217,7 +289,7 @@
 
         // Now delete "foo" and add "bar".
         deleteWord(uri);
-        addWord("bar", Locale.US, 18);
+        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.
@@ -241,10 +313,11 @@
         Log.d(TAG, "testClose");
 
         // Insert "foo".
-        Uri uri = addWord("foo", Locale.US, 17);
+        Uri uri = addWord("foo", Locale.US, 17, null);
 
         // Create the UserDictionaryLookup and wait until it's loaded.
-        UserDictionaryLookup lookup = new UserDictionaryLookup(mContext);
+        UserDictionaryLookup lookup = new UserDictionaryLookup(mContext, ExecutorUtils.SPELLING);
+        lookup.open();
         while (!lookup.isLoaded()) {
         }
 
@@ -259,7 +332,7 @@
 
         // Now delete "foo" and add "bar".
         deleteWord(uri);
-        addWord("bar", Locale.US, 18);
+        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.
diff --git a/tests/src/com/android/inputmethod/latin/utils/AsyncResultHolderTests.java b/tests/src/com/android/inputmethod/latin/utils/AsyncResultHolderTests.java
index 170d643..c214b5f 100644
--- a/tests/src/com/android/inputmethod/latin/utils/AsyncResultHolderTests.java
+++ b/tests/src/com/android/inputmethod/latin/utils/AsyncResultHolderTests.java
@@ -45,27 +45,27 @@
     }
 
     public void testGetWithoutSet() {
-        final AsyncResultHolder<Integer> holder = new AsyncResultHolder<>();
+        final AsyncResultHolder<Integer> holder = new AsyncResultHolder<>("Test");
         final int resultValue = holder.get(DEFAULT_VALUE, TIMEOUT_IN_MILLISECONDS);
         assertEquals(DEFAULT_VALUE, resultValue);
     }
 
     public void testGetBeforeSet() {
-        final AsyncResultHolder<Integer> holder = new AsyncResultHolder<>();
+        final AsyncResultHolder<Integer> holder = new AsyncResultHolder<>("Test");
         setAfterGivenTime(holder, SET_VALUE, TIMEOUT_IN_MILLISECONDS + MARGIN_IN_MILLISECONDS);
         final int resultValue = holder.get(DEFAULT_VALUE, TIMEOUT_IN_MILLISECONDS);
         assertEquals(DEFAULT_VALUE, resultValue);
     }
 
     public void testGetAfterSet() {
-        final AsyncResultHolder<Integer> holder = new AsyncResultHolder<>();
+        final AsyncResultHolder<Integer> holder = new AsyncResultHolder<>("Test");
         holder.set(SET_VALUE);
         final int resultValue = holder.get(DEFAULT_VALUE, TIMEOUT_IN_MILLISECONDS);
         assertEquals(SET_VALUE, resultValue);
     }
 
     public void testGetBeforeTimeout() {
-        final AsyncResultHolder<Integer> holder = new AsyncResultHolder<>();
+        final AsyncResultHolder<Integer> holder = new AsyncResultHolder<>("Test");
         setAfterGivenTime(holder, SET_VALUE, TIMEOUT_IN_MILLISECONDS - MARGIN_IN_MILLISECONDS);
         final int resultValue = holder.get(DEFAULT_VALUE, TIMEOUT_IN_MILLISECONDS);
         assertEquals(SET_VALUE, resultValue);
diff --git a/tests/src/com/android/inputmethod/latin/utils/CollectionUtilsTests.java b/tests/src/com/android/inputmethod/latin/utils/CollectionUtilsTests.java
index 47fd5fe..6871a41 100644
--- a/tests/src/com/android/inputmethod/latin/utils/CollectionUtilsTests.java
+++ b/tests/src/com/android/inputmethod/latin/utils/CollectionUtilsTests.java
@@ -24,6 +24,9 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 
 /**
  * Tests for {@link CollectionUtils}.
@@ -79,9 +82,13 @@
      * results for a few cases.
      */
     public void testIsNullOrEmpty() {
-        assertTrue(CollectionUtils.isNullOrEmpty(null));
-        assertTrue(CollectionUtils.isNullOrEmpty(new ArrayList<>()));
-        assertTrue(CollectionUtils.isNullOrEmpty(Collections.EMPTY_SET));
-        assertFalse(CollectionUtils.isNullOrEmpty(Collections.singleton("Not empty")));
+        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));
+        assertFalse(CollectionUtils.isNullOrEmpty(Collections.singletonList("Not empty")));
+        assertFalse(CollectionUtils.isNullOrEmpty(Collections.singletonMap("Not", "empty")));
     }
 }