Merge "Add affinity model for contact names."
diff --git a/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java b/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java
index ee142d8..09f8032 100644
--- a/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java
+++ b/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java
@@ -377,7 +377,8 @@
             final ContentValues values = MetadataDbHelper.makeContentValues(0,
                     MetadataDbHelper.TYPE_BULK, MetadataDbHelper.STATUS_INSTALLED,
                     mWordList.mId, mWordList.mLocale, mWordList.mDescription,
-                    "", mWordList.mRemoteFilename, mWordList.mLastUpdate,
+                    TextUtils.isEmpty(mWordList.mLocalFilename) ? "" : mWordList.mLocalFilename,
+                    mWordList.mRemoteFilename, mWordList.mLastUpdate,
                     mWordList.mRawChecksum, mWordList.mChecksum, mWordList.mRetryCount,
                     mWordList.mFileSize, mWordList.mVersion, mWordList.mFormatVersion);
             PrivateLog.log("Insert 'preinstalled' record for " + mWordList.mDescription
diff --git a/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java b/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java
index e720f3c..e61547a 100644
--- a/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java
+++ b/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java
@@ -95,6 +95,8 @@
     // Name of the category for the main dictionary
     public static final String MAIN_DICTIONARY_CATEGORY = "main";
 
+    public static final String TEMP_DICT_FILE_SUB = "___";
+
     // The id for the "dictionary available" notification.
     static final int DICT_AVAILABLE_NOTIFICATION_ID = 1;
 
@@ -743,7 +745,7 @@
             throws IOException {
         DebugLogUtils.l("Entering openTempFileOutput");
         final File dir = context.getFilesDir();
-        final File f = File.createTempFile(locale + "___", DICT_FILE_SUFFIX, dir);
+        final File f = File.createTempFile(locale + TEMP_DICT_FILE_SUB, DICT_FILE_SUFFIX, dir);
         DebugLogUtils.l("File name is", f.getName());
         return f.getName();
     }
diff --git a/java/src/com/android/inputmethod/latin/PersonalDictionaryLookup.java b/java/src/com/android/inputmethod/latin/PersonalDictionaryLookup.java
index 1ba075c..eed4ec1 100644
--- a/java/src/com/android/inputmethod/latin/PersonalDictionaryLookup.java
+++ b/java/src/com/android/inputmethod/latin/PersonalDictionaryLookup.java
@@ -40,9 +40,9 @@
 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 java.util.concurrent.atomic.AtomicBoolean;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -196,11 +196,10 @@
     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.
+     * We store a map from a dictionary word to the set of locales & raw string(as it appears)
+     * We then iterate over the set of locales to find a match using LocaleUtils.
      */
-    private volatile HashMap<String, ArrayList<Locale>> mDictWords;
+    private volatile HashMap<String, HashMap<Locale, String>> mDictWords;
 
     /**
      * We store a map from a shortcut to a word for each locale.
@@ -317,7 +316,7 @@
      * @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;
+        final HashMap<String, HashMap<Locale, String>> dictWords = mDictWords;
         if (CollectionUtils.isNullOrEmpty(dictWords)) {
             return Collections.emptySet();
         }
@@ -325,12 +324,15 @@
         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);
-                }
+            HashMap<Locale, String> localeStringMap = dictWords.get(word);
+                if (!CollectionUtils.isNullOrEmpty(localeStringMap)) {
+                    for (Locale wordLocale : localeStringMap.keySet()) {
+                        final String wordLocaleString = wordLocale.toString();
+                        final int match = LocaleUtils.getMatchLevel(wordLocaleString, inputLocaleString);
+                        if (LocaleUtils.isMatch(match)) {
+                            words.add(localeStringMap.get(wordLocale));
+                        }
+                    }
             }
         }
         return words;
@@ -399,29 +401,29 @@
             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 + "]");
         }
+        // Atomically obtain the current copy of mDictWords;
+        final HashMap<String, HashMap<Locale, String>> dictWords = mDictWords;
         // 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) {
+        final HashMap<Locale, String> dictLocales = dictWords.get(lowercased);
+
+        if (CollectionUtils.isNullOrEmpty(dictLocales)) {
             if (DebugFlags.DEBUG_ENABLED) {
-                Log.d(mTag, "isValidWord() : No entry for lowercased word [" + lowercased + "]");
+                Log.d(mTag, "isValidWord() : No entry for word [" + word + "]");
             }
             return false;
         } else {
             if (DebugFlags.DEBUG_ENABLED) {
-                Log.d(mTag, "isValidWord() : Found entry for lowercased word [" + lowercased + "]");
+                Log.d(mTag, "isValidWord() : Found entry for word [" + word + "]");
             }
             // Iterate over the locales this word is in.
-            for (final Locale dictLocale : dictLocales) {
+            for (final Locale dictLocale : dictLocales.keySet()) {
                 final int matchLevel = LocaleUtils.getMatchLevel(dictLocale.toString(),
                         inputLocale.toString());
                 if (DebugFlags.DEBUG_ENABLED) {
@@ -529,7 +531,7 @@
             return;
         }
         Log.i(mTag, "loadPersonalDictionary() : Start Loading");
-        HashMap<String, ArrayList<Locale>> dictWords = new HashMap<>();
+        HashMap<String, HashMap<Locale, String>> 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,
@@ -581,21 +583,21 @@
                 final String dictWord = rawDictWord.toLowerCase(dictLocale);
                 if (DebugFlags.DEBUG_ENABLED) {
                     Log.d(mTag, "loadPersonalDictionary() : Adding word [" + dictWord
-                            + "] for locale " + dictLocale);
+                            + "] for locale " + dictLocale + "with value" + rawDictWord);
                 }
                 // Check if there is an existing entry for this word.
-                ArrayList<Locale> dictLocales = dictWords.get(dictWord);
-                if (null == dictLocales) {
+                HashMap<Locale, String> dictLocales = dictWords.get(dictWord);
+                if (CollectionUtils.isNullOrEmpty(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<>();
+                    dictLocales = new HashMap<>();
                     dictWords.put(dictWord, dictLocales);
                 }
                 // Append the locale to the list of locales this word is in.
-                dictLocales.add(dictLocale);
+                dictLocales.put(dictLocale, rawDictWord);
 
                 // If there is no column for a shortcut, we're done.
                 final int shortcutIndex = cursor.getColumnIndex(UserDictionary.Words.SHORTCUT);
diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java
index 49c47d7..a123d28 100644
--- a/java/src/com/android/inputmethod/latin/RichInputConnection.java
+++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java
@@ -48,6 +48,7 @@
 import com.android.inputmethod.latin.utils.TextRange;
 
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
 /**
  * Enrichment class for InputConnection to simplify interaction and add functionality.
@@ -288,6 +289,7 @@
         }
     }
 
+    @Nullable
     public CharSequence getSelectedText(final int flags) {
         return isConnected() ?  mIC.getSelectedText(flags) : null;
     }
diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
index 4bee94a..324ae3a 100644
--- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
+++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
@@ -1089,8 +1089,11 @@
                 // If there is a selection, remove it.
                 // We also need to unlearn the selected text.
                 final CharSequence selection = mConnection.getSelectedText(0 /* 0 for no styles */);
-                unlearnWord(selection.toString(), inputTransaction.mSettingsValues,
-                        Constants.EVENT_BACKSPACE);
+                if (!TextUtils.isEmpty(selection)) {
+                    unlearnWord(selection.toString(), inputTransaction.mSettingsValues,
+                            Constants.EVENT_BACKSPACE);
+                    hasUnlearnedWordBeingDeleted = true;
+                }
                 final int numCharsDeleted = mConnection.getExpectedSelectionEnd()
                         - mConnection.getExpectedSelectionStart();
                 mConnection.setSelection(mConnection.getExpectedSelectionEnd(),
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
index fd5c54c..d6de945 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
@@ -58,6 +58,9 @@
     protected final SuggestionsCache mSuggestionsCache = new SuggestionsCache();
     private final ContentObserver mObserver;
 
+    private static final String quotesRegexp =
+            "\\u0022|\\u0027|\\u0060|\\u00B4|\\u2018|\\u2018|\\u201C|\\u201D";
+
     private static final class SuggestionsParams {
         public final String[] mSuggestions;
         public final int mFlags;
@@ -224,12 +227,16 @@
     protected SuggestionsInfo onGetSuggestionsInternal(
             final TextInfo textInfo, final NgramContext ngramContext, final int suggestionsLimit) {
         try {
-            final String inText = textInfo.getText();
+            final String text = textInfo.getText().
+                    replaceAll(AndroidSpellCheckerService.APOSTROPHE,
+                            AndroidSpellCheckerService.SINGLE_QUOTE).
+                    replaceAll("^" + quotesRegexp, "").
+                    replaceAll(quotesRegexp + "$", "");
             final SuggestionsParams cachedSuggestionsParams =
-                    mSuggestionsCache.getSuggestionsFromCache(inText, ngramContext);
+                    mSuggestionsCache.getSuggestionsFromCache(text, ngramContext);
 
             if (cachedSuggestionsParams != null) {
-                Log.d(TAG, "onGetSuggestionsInternal() : Cache hit for [" + inText + "]");
+                Log.d(TAG, "onGetSuggestionsInternal() : Cache hit for [" + text + "]");
                 return new SuggestionsInfo(
                         cachedSuggestionsParams.mFlags, cachedSuggestionsParams.mSuggestions);
             }
@@ -241,10 +248,10 @@
             }
 
             // Handle special patterns like email, URI, telephone number.
-            final int checkability = getCheckabilityInScript(inText, mScript);
+            final int checkability = getCheckabilityInScript(text, mScript);
             if (CHECKABILITY_CHECKABLE != checkability) {
                 if (CHECKABILITY_CONTAINS_PERIOD == checkability) {
-                    final String[] splitText = inText.split(Constants.REGEXP_PERIOD);
+                    final String[] splitText = text.split(Constants.REGEXP_PERIOD);
                     boolean allWordsAreValid = true;
                     for (final String word : splitText) {
                         if (!mService.isValidWord(mLocale, word)) {
@@ -259,15 +266,13 @@
                                         TextUtils.join(Constants.STRING_SPACE, splitText) });
                     }
                 }
-                return mService.isValidWord(mLocale, inText) ?
+                return mService.isValidWord(mLocale, text) ?
                         AndroidSpellCheckerService.getInDictEmptySuggestions() :
                         AndroidSpellCheckerService.getNotInDictEmptySuggestions(
                                 CHECKABILITY_CONTAINS_PERIOD == checkability /* reportAsTypo */);
             }
 
             // Handle normal words.
-            final String text = inText.replaceAll(
-                    AndroidSpellCheckerService.APOSTROPHE, AndroidSpellCheckerService.SINGLE_QUOTE);
             final int capitalizeType = StringUtils.getCapitalizationType(text);
 
             if (isInDictForAnyCapitalization(text, capitalizeType)) {
diff --git a/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java b/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java
index 25fa723..cfa977a 100644
--- a/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java
@@ -25,6 +25,7 @@
 import android.view.inputmethod.InputMethodSubtype;
 
 import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.dictionarypack.UpdateHandler;
 import com.android.inputmethod.latin.AssetFileAddress;
 import com.android.inputmethod.latin.BinaryDictionaryGetter;
 import com.android.inputmethod.latin.R;
@@ -36,6 +37,7 @@
 import com.android.inputmethod.latin.settings.SpacingAndPunctuations;
 
 import java.io.File;
+import java.io.FilenameFilter;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Iterator;
@@ -58,6 +60,8 @@
     // 6 digits - unicode is limited to 21 bits
     private static final int MAX_HEX_DIGITS_FOR_CODEPOINT = 6;
 
+    private static final String TEMP_DICT_FILE_SUB = UpdateHandler.TEMP_DICT_FILE_SUB;
+
     public static class DictionaryInfo {
         private static final String LOCALE_COLUMN = "locale";
         private static final String WORDLISTID_COLUMN = "id";
@@ -66,22 +70,24 @@
         private static final String DATE_COLUMN = "date";
         private static final String FILESIZE_COLUMN = "filesize";
         private static final String VERSION_COLUMN = "version";
-        @Nonnull
-        public final String mId;
-        @Nonnull
-        public final Locale mLocale;
-        @Nullable
-        public final String mDescription;
-        public final AssetFileAddress mFileAddress;
+
+        @Nonnull public final String mId;
+        @Nonnull public final Locale mLocale;
+        @Nullable public final String mDescription;
+        @Nullable public final String mFilename;
+        public final long mFilesize;
+        public final long mModifiedTimeMillis;
         public final int mVersion;
 
-        public DictionaryInfo(@Nonnull final String id, @Nonnull final Locale locale,
-                @Nullable final String description, @Nullable final AssetFileAddress fileAddress,
-                final int version) {
+        public DictionaryInfo(@Nonnull String id, @Nonnull Locale locale,
+                @Nullable String description, @Nullable String filename,
+                long filesize, long modifiedTimeMillis, int version) {
             mId = id;
             mLocale = locale;
             mDescription = description;
-            mFileAddress = fileAddress;
+            mFilename = filename;
+            mFilesize = filesize;
+            mModifiedTimeMillis = modifiedTimeMillis;
             mVersion = version;
         }
 
@@ -90,12 +96,9 @@
             values.put(WORDLISTID_COLUMN, mId);
             values.put(LOCALE_COLUMN, mLocale.toString());
             values.put(DESCRIPTION_COLUMN, mDescription);
-            values.put(LOCAL_FILENAME_COLUMN,
-                    mFileAddress != null ? mFileAddress.mFilename : "");
-            values.put(DATE_COLUMN, TimeUnit.MILLISECONDS.toSeconds(
-                    mFileAddress != null ? new File(mFileAddress.mFilename).lastModified() : 0));
-            values.put(FILESIZE_COLUMN,
-                    mFileAddress != null ? mFileAddress.mLength : 0);
+            values.put(LOCAL_FILENAME_COLUMN, mFilename != null ? mFilename : "");
+            values.put(DATE_COLUMN, TimeUnit.MILLISECONDS.toSeconds(mModifiedTimeMillis));
+            values.put(FILESIZE_COLUMN, mFilesize);
             values.put(VERSION_COLUMN, mVersion);
             return values;
         }
@@ -185,6 +188,17 @@
         return new File(DictionaryInfoUtils.getWordListCacheDirectory(context)).listFiles();
     }
 
+    @Nullable
+    public static File[] getUnusedDictionaryList(final Context context) {
+        return context.getFilesDir().listFiles(new FilenameFilter() {
+            @Override
+            public boolean accept(File dir, String filename) {
+                return !TextUtils.isEmpty(filename) && filename.endsWith(".dict")
+                        && filename.contains(TEMP_DICT_FILE_SUB);
+            }
+        });
+    }
+
     /**
      * Returns the category for a given file name.
      *
@@ -342,12 +356,44 @@
      * @return information of the specified dictionary.
      */
     private static DictionaryInfo createDictionaryInfoFromFileAddress(
-            final AssetFileAddress fileAddress, Locale locale) {
+            @Nonnull final AssetFileAddress fileAddress, final Locale locale) {
         final String id = getMainDictId(locale);
         final int version = DictionaryHeaderUtils.getContentVersion(fileAddress);
         final String description = SubtypeLocaleUtils
                 .getSubtypeLocaleDisplayName(locale.toString());
-        return new DictionaryInfo(id, locale, description, fileAddress, version);
+        // Do not store the filename on db as it will try to move the filename from db to the
+        // cached directory. If the filename is already in cached directory, this is not
+        // necessary.
+        final String filenameToStoreOnDb = null;
+        return new DictionaryInfo(id, locale, description, filenameToStoreOnDb,
+                fileAddress.mLength, new File(fileAddress.mFilename).lastModified(), version);
+    }
+
+    /**
+     * Returns the information of the dictionary for the given {@link AssetFileAddress}.
+     * If the file is corrupted or a pre-fava file, then the file gets deleted and the null
+     * value is returned.
+     */
+    @Nullable
+    private static DictionaryInfo createDictionaryInfoForUnCachedFile(
+            @Nonnull final AssetFileAddress fileAddress, final Locale locale) {
+        final String id = getMainDictId(locale);
+        final int version = DictionaryHeaderUtils.getContentVersion(fileAddress);
+
+        if (version == -1) {
+            // Purge the pre-fava/corrupted unused dictionaires.
+            fileAddress.deleteUnderlyingFile();
+            return null;
+        }
+
+        final String description = SubtypeLocaleUtils
+                .getSubtypeLocaleDisplayName(locale.toString());
+
+        final File unCachedFile = new File(fileAddress.mFilename);
+        // Store just the filename and not the full path.
+        final String filenameToStoreOnDb = unCachedFile.getName();
+        return new DictionaryInfo(id, locale, description, filenameToStoreOnDb, fileAddress.mLength,
+                unCachedFile.lastModified(), version);
     }
 
     /**
@@ -358,7 +404,7 @@
         final int version = -1;
         final String description = SubtypeLocaleUtils
                 .getSubtypeLocaleDisplayName(locale.toString());
-        return new DictionaryInfo(id, locale, description, null, version);
+        return new DictionaryInfo(id, locale, description, null, 0L, 0L, version);
     }
 
     private static void addOrUpdateDictInfo(final ArrayList<DictionaryInfo> dictList,
@@ -380,7 +426,7 @@
             final Context context) {
         final ArrayList<DictionaryInfo> dictList = new ArrayList<>();
 
-        // Retrieve downloaded dictionaries
+        // Retrieve downloaded dictionaries from cached directories
         final File[] directoryList = getCachedDirectoryList(context);
         if (null != directoryList) {
             for (final File directory : directoryList) {
@@ -407,6 +453,25 @@
             }
         }
 
+        // Retrieve downloaded dictionaries from the unused dictionaries.
+        File[] unusedDictionaryList = getUnusedDictionaryList(context);
+        if (unusedDictionaryList != null) {
+            for (File dictionaryFile : unusedDictionaryList) {
+                String fileName = dictionaryFile.getName();
+                int index = fileName.indexOf(TEMP_DICT_FILE_SUB);
+                if (index == -1) {
+                    continue;
+                }
+                String locale = fileName.substring(0, index);
+                DictionaryInfo dictionaryInfo = createDictionaryInfoForUnCachedFile(
+                        AssetFileAddress.makeFromFile(dictionaryFile),
+                        LocaleUtils.constructLocaleFromString(locale));
+                if (dictionaryInfo != null) {
+                    addOrUpdateDictInfo(dictList, dictionaryInfo);
+                }
+            }
+        }
+
         // Retrieve files from assets
         final Resources resources = context.getResources();
         final AssetManager assets = resources.getAssets();
diff --git a/tests/src/com/android/inputmethod/latin/PersonalDictionaryLookupTest.java b/tests/src/com/android/inputmethod/latin/PersonalDictionaryLookupTest.java
index 983957f..c06aded 100644
--- a/tests/src/com/android/inputmethod/latin/PersonalDictionaryLookupTest.java
+++ b/tests/src/com/android/inputmethod/latin/PersonalDictionaryLookupTest.java
@@ -289,7 +289,8 @@
         addWord("fOo", Locale.FRENCH, 17, null);
 
         // Create the PersonalDictionaryLookup and wait until it's loaded.
-        PersonalDictionaryLookup lookup = new PersonalDictionaryLookup(mContext, ExecutorUtils.SPELLING);
+        PersonalDictionaryLookup lookup = new PersonalDictionaryLookup(mContext,
+                ExecutorUtils.SPELLING);
         lookup.open();
 
         // Both en_CA and en_US match.
@@ -304,6 +305,43 @@
         lookup.close();
     }
 
+
+    public void testCaseMatchingForWordsAndShortcuts() {
+        Log.d(TAG, "testCaseMatchingForWordsAndShortcuts");
+        addWord("Foo", Locale.US, 17, "f");
+        addWord("bokabu", Locale.US, 17, "Bu");
+
+        // Create the PersonalDictionaryLookup and wait until it's loaded.
+        PersonalDictionaryLookup lookup = new PersonalDictionaryLookup(mContext,
+                ExecutorUtils.SPELLING);
+        lookup.open();
+
+        // Valid, inspite of capitalization in US but not in other
+        // locales.
+        assertTrue(lookup.isValidWord("Foo", Locale.US));
+        assertTrue(lookup.isValidWord("foo", Locale.US));
+        assertFalse(lookup.isValidWord("Foo", Locale.UK));
+        assertFalse(lookup.isValidWord("foo", Locale.UK));
+
+        // Valid in all forms in US.
+        assertTrue(lookup.isValidWord("bokabu", Locale.US));
+        assertTrue(lookup.isValidWord("BOKABU", Locale.US));
+        assertTrue(lookup.isValidWord("BokaBU", Locale.US));
+
+        // Correct capitalization; sensitive to shortcut casing & locale.
+        assertEquals("Foo", lookup.expandShortcut("f", Locale.US));
+        assertNull(lookup.expandShortcut("f", Locale.UK));
+
+        // Correct capitalization; sensitive to shortcut casing & locale.
+        assertEquals("bokabu", lookup.expandShortcut("Bu", Locale.US));
+        assertNull(lookup.expandShortcut("Bu", Locale.UK));
+        assertNull(lookup.expandShortcut("bu", Locale.US));
+
+        // Verify that raw strings are retained for #getWordsForLocale.
+        verifyWordExists(lookup.getWordsForLocale(Locale.US), "Foo");
+        verifyWordDoesNotExist(lookup.getWordsForLocale(Locale.US), "foo");
+    }
+
     public void testManageListeners() {
         Log.d(TAG, "testManageListeners");