Merge "Remove or rename native codes for ver3 dictionary format."
diff --git a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
index e8a8971..1400e05 100644
--- a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
@@ -1008,6 +1008,12 @@
         final int index = me.getActionIndex();
         final int id = me.getPointerId(index);
         final PointerTracker tracker = PointerTracker.getPointerTracker(id);
+        // When a more keys panel is showing, we should ignore other fingers' single touch events
+        // other than the finger that is showing the more keys panel.
+        if (isShowingMoreKeysPanel() && !tracker.isShowingMoreKeysPanel()
+                && PointerTracker.getActivePointerTrackerCount() == 1) {
+            return true;
+        }
         tracker.processMotionEvent(me, mKeyDetector);
         return true;
     }
diff --git a/java/src/com/android/inputmethod/keyboard/PointerTracker.java b/java/src/com/android/inputmethod/keyboard/PointerTracker.java
index ae6aee4..f9e78bf 100644
--- a/java/src/com/android/inputmethod/keyboard/PointerTracker.java
+++ b/java/src/com/android/inputmethod/keyboard/PointerTracker.java
@@ -715,7 +715,7 @@
         return newKey;
     }
 
-    private static int getActivePointerTrackerCount() {
+    /* package */ static int getActivePointerTrackerCount() {
         return sPointerTrackerQueue.size();
     }
 
@@ -827,12 +827,19 @@
         final int action = me.getActionMasked();
         final long eventTime = me.getEventTime();
         if (action == MotionEvent.ACTION_MOVE) {
+            // When this pointer is the only active pointer and is showing a more keys panel,
+            // we should ignore other pointers' motion event.
+            final boolean shouldIgnoreOtherPointers =
+                    isShowingMoreKeysPanel() && getActivePointerTrackerCount() == 1;
             final int pointerCount = me.getPointerCount();
             for (int index = 0; index < pointerCount; index++) {
                 final int id = me.getPointerId(index);
-                final PointerTracker tracker = getPointerTracker(id);
+                if (shouldIgnoreOtherPointers && id != mPointerId) {
+                    continue;
+                }
                 final int x = (int)me.getX(index);
                 final int y = (int)me.getY(index);
+                final PointerTracker tracker = getPointerTracker(id);
                 tracker.onMoveEvent(x, y, eventTime, me);
             }
             return;
@@ -903,7 +910,7 @@
         }
     }
 
-    private boolean isShowingMoreKeysPanel() {
+    /* package */ boolean isShowingMoreKeysPanel() {
         return (mMoreKeysPanel != null);
     }
 
diff --git a/java/src/com/android/inputmethod/keyboard/internal/DynamicGridKeyboard.java b/java/src/com/android/inputmethod/keyboard/internal/DynamicGridKeyboard.java
index f0feb25..e2fd390 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/DynamicGridKeyboard.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/DynamicGridKeyboard.java
@@ -25,7 +25,7 @@
 import com.android.inputmethod.keyboard.Keyboard;
 import com.android.inputmethod.latin.settings.Settings;
 import com.android.inputmethod.latin.utils.CollectionUtils;
-import com.android.inputmethod.latin.utils.StringUtils;
+import com.android.inputmethod.latin.utils.JsonUtils;
 
 import java.util.ArrayDeque;
 import java.util.ArrayList;
@@ -139,7 +139,7 @@
                 keys.add(key.getCode());
             }
         }
-        final String jsonStr = StringUtils.listToJsonStr(keys);
+        final String jsonStr = JsonUtils.listToJsonStr(keys);
         Settings.writeEmojiRecentKeys(mPrefs, jsonStr);
     }
 
@@ -167,7 +167,7 @@
 
     public void loadRecentKeys(final Collection<DynamicGridKeyboard> keyboards) {
         final String str = Settings.readEmojiRecentKeys(mPrefs);
-        final List<Object> keys = StringUtils.jsonStrToList(str);
+        final List<Object> keys = JsonUtils.jsonStrToList(str);
         for (final Object o : keys) {
             final Key key;
             if (o instanceof Integer) {
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java
index f1b5bc1..1b722c5 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java
@@ -384,7 +384,8 @@
         /* 122 */ "w",
         /* 123 */ "y",
         /* 124 */ "x",
-        /* 125 */ EMPTY,
+        // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE
+        /* 125 */ "\u00F1",
         /* 126 */ "!fixedColumnOrder!2,!hasLabels!,!text/label_time_am,!text/label_time_pm",
         /* 127 */ "!icon/settings_key|!code/key_settings",
         /* 128 */ "!icon/shortcut_key|!code/key_shortcut",
@@ -1286,15 +1287,6 @@
         // U+00A1: "¡" INVERTED EXCLAMATION MARK
         // U+00BF: "¿" INVERTED QUESTION MARK
         /* 59 */ "!fixedColumnOrder!9,\u00A1,;,/,(,),#,!,\\,,?,\u00BF,&,\\%,+,\",-,:,',@",
-        /* 60~ */
-        null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
-        null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
-        null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
-        null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
-        null, null, null, null, null,
-        /* ~124 */
-        // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE
-        /* 125 */ "\u00F1",
     };
 
     /* Language et_EE: Estonian (Estonia) */
diff --git a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
index b70362f..b6cfcd0 100644
--- a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
@@ -72,7 +72,7 @@
     private final boolean mUseFirstLastBigrams;
 
     public ContactsBinaryDictionary(final Context context, final Locale locale) {
-        super(context, getFilenameWithLocale(NAME, locale), locale,
+        super(context, getDictNameWithLocale(NAME, locale), locale,
                 Dictionary.TYPE_CONTACTS, false /* isUpdatable */);
         mLocale = locale;
         mUseFirstLastBigrams = useFirstLastBigramsForLocale(locale);
diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
index 5d1c396..b52045e 100644
--- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
@@ -69,14 +69,14 @@
     /**
      * A static map of update controllers, each of which records the time of accesses to a single
      * binary dictionary file and tracks whether the file is regenerating. The key for this map is
-     * the filename and the value is the shared dictionary time recorder associated with that
-     * filename.
+     * the dictionary name  and the value is the shared dictionary time recorder associated with
+     * that dictionary name.
      */
     private static final ConcurrentHashMap<String, DictionaryUpdateController>
-            sFilenameDictionaryUpdateControllerMap = CollectionUtils.newConcurrentHashMap();
+            sDictNameDictionaryUpdateControllerMap = CollectionUtils.newConcurrentHashMap();
 
     private static final ConcurrentHashMap<String, PrioritizedSerialExecutor>
-            sFilenameExecutorMap = CollectionUtils.newConcurrentHashMap();
+            sDictNameExecutorMap = CollectionUtils.newConcurrentHashMap();
 
     /** The application context. */
     protected final Context mContext;
@@ -92,11 +92,11 @@
     protected AbstractDictionaryWriter mDictionaryWriter;
 
     /**
-     * The name of this dictionary, used as the filename for storing the binary dictionary. Multiple
-     * dictionary instances with the same filename is supported, with access controlled by
-     * DictionaryTimeRecorder.
+     * The name of this dictionary, used as a part of the filename for storing the binary
+     * dictionary. Multiple dictionary instances with the same name is supported, with access
+     * controlled by DictionaryUpdateController.
      */
-    private final String mFilename;
+    private final String mDictName;
 
     /** Dictionary locale */
     private final Locale mLocale;
@@ -106,7 +106,7 @@
 
     // TODO: remove, once dynamic operations is serialized
     /** Controls updating the shared binary dictionary file across multiple instances. */
-    private final DictionaryUpdateController mFilenameDictionaryUpdateController;
+    private final DictionaryUpdateController mDictNameDictionaryUpdateController;
 
     // TODO: remove, once dynamic operations is serialized
     /** Controls updating the local binary dictionary for this instance. */
@@ -144,34 +144,46 @@
         return mBinaryDictionary.isValidDictionary();
     }
 
-    protected String getFileNameExtensionToOpenDict() {
-        return "";
+    protected String getFileNameToCreateDict(final String dictName) {
+        return dictName + DICT_FILE_EXTENSION;
+    }
+
+    protected String getFileNameToOpenDict(final String dictName) {
+        return getFileNameToCreateDict(dictName);
+    }
+
+    private File getFileToCreateDict() {
+        return new File(mContext.getFilesDir(), getFileNameToCreateDict(mDictName));
+    }
+
+    private File getFileToOpenDict() {
+        return new File(mContext.getFilesDir(), getFileNameToOpenDict(mDictName));
     }
 
     /**
-     * Gets the dictionary update controller for the given filename.
+     * Gets the dictionary update controller for the given dictionary name.
      */
     private static DictionaryUpdateController getDictionaryUpdateController(
-            String filename) {
-        DictionaryUpdateController recorder = sFilenameDictionaryUpdateControllerMap.get(filename);
+            final String dictName) {
+        DictionaryUpdateController recorder = sDictNameDictionaryUpdateControllerMap.get(dictName);
         if (recorder == null) {
-            synchronized(sFilenameDictionaryUpdateControllerMap) {
+            synchronized(sDictNameDictionaryUpdateControllerMap) {
                 recorder = new DictionaryUpdateController();
-                sFilenameDictionaryUpdateControllerMap.put(filename, recorder);
+                sDictNameDictionaryUpdateControllerMap.put(dictName, recorder);
             }
         }
         return recorder;
     }
 
     /**
-     * Gets the executor for the given filename.
+     * Gets the executor for the given dictionary name.
      */
-    private static PrioritizedSerialExecutor getExecutor(final String filename) {
-        PrioritizedSerialExecutor executor = sFilenameExecutorMap.get(filename);
+    private static PrioritizedSerialExecutor getExecutor(final String dictName) {
+        PrioritizedSerialExecutor executor = sDictNameExecutorMap.get(dictName);
         if (executor == null) {
-            synchronized(sFilenameExecutorMap) {
+            synchronized(sDictNameExecutorMap) {
                 executor = new PrioritizedSerialExecutor();
-                sFilenameExecutorMap.put(filename, executor);
+                sDictNameExecutorMap.put(dictName, executor);
             }
         }
         return executor;
@@ -190,28 +202,28 @@
      * Creates a new expandable binary dictionary.
      *
      * @param context The application context of the parent.
-     * @param filename The filename for this binary dictionary. Multiple dictionaries with the same
-     *        filename is supported.
+     * @param dictName The name of the dictionary. Multiple instances with the same
+     *        name is supported.
      * @param locale the dictionary locale.
      * @param dictType the dictionary type, as a human-readable string
      * @param isUpdatable whether to support dynamically updating the dictionary. Please note that
      *        dynamic dictionary has negative effects on memory space and computation time.
      */
-    public ExpandableBinaryDictionary(final Context context, final String filename,
+    public ExpandableBinaryDictionary(final Context context, final String dictName,
             final Locale locale, final String dictType, final boolean isUpdatable) {
         super(dictType);
-        mFilename = filename;
+        mDictName = dictName;
         mContext = context;
         mLocale = locale;
         mIsUpdatable = isUpdatable;
         mBinaryDictionary = null;
-        mFilenameDictionaryUpdateController = getDictionaryUpdateController(filename);
+        mDictNameDictionaryUpdateController = getDictionaryUpdateController(dictName);
         // Currently, only dynamic personalization dictionary is updatable.
         mDictionaryWriter = getDictionaryWriter(context, isUpdatable);
     }
 
-    protected static String getFilenameWithLocale(final String name, final Locale locale) {
-        return name + "." + locale.toString() + DICT_FILE_EXTENSION;
+    protected static String getDictNameWithLocale(final String name, final Locale locale) {
+        return name + "." + locale.toString();
     }
 
     /**
@@ -219,7 +231,7 @@
      */
     @Override
     public void close() {
-        getExecutor(mFilename).execute(new Runnable() {
+        getExecutor(mDictName).execute(new Runnable() {
             @Override
             public void run() {
                 if (mBinaryDictionary!= null) {
@@ -232,7 +244,7 @@
 
     protected void closeBinaryDictionary() {
         // Ensure that no other threads are accessing the local binary dictionary.
-        getExecutor(mFilename).execute(new Runnable() {
+        getExecutor(mDictName).execute(new Runnable() {
             @Override
             public void run() {
                 if (mBinaryDictionary != null) {
@@ -245,7 +257,7 @@
 
     protected Map<String, String> getHeaderAttributeMap() {
         HashMap<String, String> attributeMap = new HashMap<String, String>();
-        attributeMap.put(FormatSpec.FileHeader.DICTIONARY_ID_ATTRIBUTE, mFilename);
+        attributeMap.put(FormatSpec.FileHeader.DICTIONARY_ID_ATTRIBUTE, mDictName);
         attributeMap.put(FormatSpec.FileHeader.DICTIONARY_LOCALE_ATTRIBUTE, mLocale.toString());
         attributeMap.put(FormatSpec.FileHeader.DICTIONARY_VERSION_ATTRIBUTE,
                 String.valueOf(TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())));
@@ -253,19 +265,18 @@
     }
 
     protected void clear() {
-        getExecutor(mFilename).execute(new Runnable() {
+        getExecutor(mDictName).execute(new Runnable() {
             @Override
             public void run() {
                 if (mDictionaryWriter == null) {
                     mBinaryDictionary.close();
-                    final File file = new File(mContext.getFilesDir(), mFilename);
+                    final File file = getFileToCreateDict();
                     file.delete();
                     BinaryDictionary.createEmptyDictFile(file.getAbsolutePath(),
                             DICTIONARY_FORMAT_VERSION, getHeaderAttributeMap());
                     // We have 'fileToOpen' in addition to 'file' for the v4 dictionary format
                     // where 'file' is a directory, and 'fileToOpen' is a normal file.
-                    final File fileToOpen = new File(mContext.getFilesDir(), mFilename
-                            + getFileNameExtensionToOpenDict());
+                    final File fileToOpen = getFileToOpenDict();
                     // TODO: Make BinaryDictionary's constructor be able to accept filename
                     // without extension.
                     mBinaryDictionary = new BinaryDictionary(
@@ -305,7 +316,7 @@
      * Check whether GC is needed and run GC if required.
      */
     protected void runGCIfRequired(final boolean mindsBlockByGC) {
-        getExecutor(mFilename).execute(new Runnable() {
+        getExecutor(mDictName).execute(new Runnable() {
             @Override
             public void run() {
                 runGCIfRequiredInternalLocked(mindsBlockByGC);
@@ -318,13 +329,13 @@
         if (mBinaryDictionary.needsToRunGC(mindsBlockByGC)) {
             if (setProcessingLargeTaskIfNot()) {
                 // Run GC after currently existing time sensitive operations.
-                getExecutor(mFilename).executePrioritized(new Runnable() {
+                getExecutor(mDictName).executePrioritized(new Runnable() {
                     @Override
                     public void run() {
                         try {
                             mBinaryDictionary.flushWithGC();
                         } finally {
-                            mFilenameDictionaryUpdateController.mProcessingLargeTask.set(false);
+                            mDictNameDictionaryUpdateController.mProcessingLargeTask.set(false);
                         }
                     }
                 });
@@ -339,10 +350,10 @@
             final String shortcutTarget, final int shortcutFreq, final boolean isNotAWord,
             final boolean isBlacklisted, final int timestamp) {
         if (!mIsUpdatable) {
-            Log.w(TAG, "addWordDynamically is called for non-updatable dictionary: " + mFilename);
+            Log.w(TAG, "addWordDynamically is called for non-updatable dictionary: " + mDictName);
             return;
         }
-        getExecutor(mFilename).execute(new Runnable() {
+        getExecutor(mDictName).execute(new Runnable() {
             @Override
             public void run() {
                 runGCIfRequiredInternalLocked(true /* mindsBlockByGC */);
@@ -359,10 +370,10 @@
             final int frequency, final int timestamp) {
         if (!mIsUpdatable) {
             Log.w(TAG, "addBigramDynamically is called for non-updatable dictionary: "
-                    + mFilename);
+                    + mDictName);
             return;
         }
-        getExecutor(mFilename).execute(new Runnable() {
+        getExecutor(mDictName).execute(new Runnable() {
             @Override
             public void run() {
                 runGCIfRequiredInternalLocked(true /* mindsBlockByGC */);
@@ -377,10 +388,10 @@
     protected void removeBigramDynamically(final String word0, final String word1) {
         if (!mIsUpdatable) {
             Log.w(TAG, "removeBigramDynamically is called for non-updatable dictionary: "
-                    + mFilename);
+                    + mDictName);
             return;
         }
-        getExecutor(mFilename).execute(new Runnable() {
+        getExecutor(mDictName).execute(new Runnable() {
             @Override
             public void run() {
                 runGCIfRequiredInternalLocked(true /* mindsBlockByGC */);
@@ -401,10 +412,10 @@
             final AddMultipleDictionaryEntriesCallback callback) {
         if (!mIsUpdatable) {
             Log.w(TAG, "addMultipleDictionaryEntriesDynamically is called for non-updatable " +
-                    "dictionary: " + mFilename);
+                    "dictionary: " + mDictName);
             return;
         }
-        getExecutor(mFilename).execute(new Runnable() {
+        getExecutor(mDictName).execute(new Runnable() {
             @Override
             public void run() {
                 final boolean locked = setProcessingLargeTaskIfNot();
@@ -417,7 +428,7 @@
                         callback.onFinished();
                     }
                     if (locked) {
-                        mFilenameDictionaryUpdateController.mProcessingLargeTask.set(false);
+                        mDictNameDictionaryUpdateController.mProcessingLargeTask.set(false);
                     }
                 }
             }
@@ -435,7 +446,7 @@
         }
         final AsyncResultHolder<ArrayList<SuggestedWordInfo>> holder =
                 new AsyncResultHolder<ArrayList<SuggestedWordInfo>>();
-        getExecutor(mFilename).executePrioritized(new Runnable() {
+        getExecutor(mDictName).executePrioritized(new Runnable() {
             @Override
             public void run() {
                 if (mBinaryDictionary == null) {
@@ -471,7 +482,7 @@
             return false;
         }
         final AsyncResultHolder<Boolean> holder = new AsyncResultHolder<Boolean>();
-        getExecutor(mFilename).executePrioritized(new Runnable() {
+        getExecutor(mDictName).executePrioritized(new Runnable() {
             @Override
             public void run() {
                 holder.set(isValidWordLocked(word));
@@ -505,23 +516,22 @@
      */
     private void loadBinaryDictionary() {
         if (DEBUG) {
-            Log.d(TAG, "Loading binary dictionary: " + mFilename + " request="
-                    + mFilenameDictionaryUpdateController.mLastUpdateRequestTime + " update="
-                    + mFilenameDictionaryUpdateController.mLastUpdateTime);
+            Log.d(TAG, "Loading binary dictionary: " + mDictName + " request="
+                    + mDictNameDictionaryUpdateController.mLastUpdateRequestTime + " update="
+                    + mDictNameDictionaryUpdateController.mLastUpdateTime);
         }
         if (DBG_STRESS_TEST) {
             // Test if this class does not cause problems when it takes long time to load binary
             // dictionary.
             try {
-                Log.w(TAG, "Start stress in loading: " + mFilename);
+                Log.w(TAG, "Start stress in loading: " + mDictName);
                 Thread.sleep(15000);
                 Log.w(TAG, "End stress in loading");
             } catch (InterruptedException e) {
             }
         }
 
-        final File file = new File(mContext.getFilesDir(), mFilename
-                + getFileNameExtensionToOpenDict());
+        final File file = getFileToOpenDict();
         final String filename = file.getAbsolutePath();
         final long length = file.length();
 
@@ -533,7 +543,7 @@
         // swapping in the new one.
         // TODO: Ensure multi-thread assignment of mBinaryDictionary.
         final BinaryDictionary oldBinaryDictionary = mBinaryDictionary;
-        getExecutor(mFilename).executePrioritized(new Runnable() {
+        getExecutor(mDictName).executePrioritized(new Runnable() {
             @Override
             public void run() {
                 mBinaryDictionary = newBinaryDictionary;
@@ -555,20 +565,20 @@
      */
     private void writeBinaryDictionary() {
         if (DEBUG) {
-            Log.d(TAG, "Generating binary dictionary: " + mFilename + " request="
-                    + mFilenameDictionaryUpdateController.mLastUpdateRequestTime + " update="
-                    + mFilenameDictionaryUpdateController.mLastUpdateTime);
+            Log.d(TAG, "Generating binary dictionary: " + mDictName + " request="
+                    + mDictNameDictionaryUpdateController.mLastUpdateRequestTime + " update="
+                    + mDictNameDictionaryUpdateController.mLastUpdateTime);
         }
         if (needsToReloadBeforeWriting()) {
             mDictionaryWriter.clear();
             loadDictionaryAsync();
-            mDictionaryWriter.write(mFilename, getHeaderAttributeMap());
+            mDictionaryWriter.write(getFileNameToCreateDict(mDictName), getHeaderAttributeMap());
         } else {
             if (mBinaryDictionary == null || !isValidDictionary()
                     // TODO: remove the check below
                     || !matchesExpectedBinaryDictFormatVersionForThisType(
                             mBinaryDictionary.getFormatVersion())) {
-                final File file = new File(mContext.getFilesDir(), mFilename);
+                final File file = getFileToCreateDict();
                 if (!FileUtils.deleteRecursively(file)) {
                     Log.e(TAG, "Can't remove a file: " + file.getName());
                 }
@@ -594,10 +604,10 @@
     protected void setRequiresReload(final boolean requiresRebuild) {
         final long time = SystemClock.uptimeMillis();
         mPerInstanceDictionaryUpdateController.mLastUpdateRequestTime = time;
-        mFilenameDictionaryUpdateController.mLastUpdateRequestTime = time;
+        mDictNameDictionaryUpdateController.mLastUpdateRequestTime = time;
         if (DEBUG) {
-            Log.d(TAG, "Reload request: " + mFilename + ": request=" + time + " update="
-                    + mFilenameDictionaryUpdateController.mLastUpdateTime);
+            Log.d(TAG, "Reload request: " + mDictName + ": request=" + time + " update="
+                    + mDictNameDictionaryUpdateController.mLastUpdateTime);
         }
     }
 
@@ -619,13 +629,13 @@
     }
 
     private boolean processingLargeTask() {
-        return mFilenameDictionaryUpdateController.mProcessingLargeTask.get();
+        return mDictNameDictionaryUpdateController.mProcessingLargeTask.get();
     }
 
     // Returns whether the dictionary is being used for a large task. If true, we should not use
     // this dictionary for latency sensitive operations.
     private boolean setProcessingLargeTaskIfNot() {
-        return mFilenameDictionaryUpdateController.mProcessingLargeTask.compareAndSet(
+        return mDictNameDictionaryUpdateController.mProcessingLargeTask.compareAndSet(
                 false /* expect */ , true /* update */);
     }
 
@@ -636,13 +646,13 @@
     private final void reloadDictionary() {
         // Ensure that only one thread attempts to read or write to the shared binary dictionary
         // file at the same time.
-        getExecutor(mFilename).execute(new Runnable() {
+        getExecutor(mDictName).execute(new Runnable() {
             @Override
             public void run() {
                 try {
                     final long time = SystemClock.uptimeMillis();
                     final boolean dictionaryFileExists = dictionaryFileExists();
-                    if (mFilenameDictionaryUpdateController.isOutOfDate()
+                    if (mDictNameDictionaryUpdateController.isOutOfDate()
                             || !dictionaryFileExists) {
                         // If the shared dictionary file does not exist or is out of date, the
                         // first instance that acquires the lock will generate a new one.
@@ -651,18 +661,18 @@
                             // rebuild the binary dictionary. Empty dictionaries are supported (in
                             // the case where loadDictionaryAsync() adds nothing) in order to
                             // provide a uniform framework.
-                            mFilenameDictionaryUpdateController.mLastUpdateTime = time;
+                            mDictNameDictionaryUpdateController.mLastUpdateTime = time;
                             writeBinaryDictionary();
                             loadBinaryDictionary();
                         } else {
                             // If not, the reload request was unnecessary so revert
                             // LastUpdateRequestTime to LastUpdateTime.
-                            mFilenameDictionaryUpdateController.mLastUpdateRequestTime =
-                                    mFilenameDictionaryUpdateController.mLastUpdateTime;
+                            mDictNameDictionaryUpdateController.mLastUpdateRequestTime =
+                                    mDictNameDictionaryUpdateController.mLastUpdateTime;
                         }
                     } else if (mBinaryDictionary == null ||
                             mPerInstanceDictionaryUpdateController.mLastUpdateTime
-                                    < mFilenameDictionaryUpdateController.mLastUpdateTime) {
+                                    < mDictNameDictionaryUpdateController.mLastUpdateTime) {
                         // Otherwise, if the local dictionary is older than the shared dictionary,
                         // load the shared dictionary.
                         loadBinaryDictionary();
@@ -670,7 +680,7 @@
                     // If we just loaded the binary dictionary, then mBinaryDictionary is not
                     // up-to-date yet so it's useless to test it right away. Schedule the check
                     // for right after it's loaded instead.
-                    getExecutor(mFilename).executePrioritized(new Runnable() {
+                    getExecutor(mDictName).executePrioritized(new Runnable() {
                         @Override
                         public void run() {
                             if (mBinaryDictionary != null && !(isValidDictionary()
@@ -680,7 +690,7 @@
                                 // Binary dictionary or its format version is not valid. Regenerate
                                 // the dictionary file. writeBinaryDictionary will remove the
                                 // existing files if appropriate.
-                                mFilenameDictionaryUpdateController.mLastUpdateTime = time;
+                                mDictNameDictionaryUpdateController.mLastUpdateTime = time;
                                 writeBinaryDictionary();
                                 loadBinaryDictionary();
                             }
@@ -688,7 +698,7 @@
                         }
                     });
                 } finally {
-                    mFilenameDictionaryUpdateController.mProcessingLargeTask.set(false);
+                    mDictNameDictionaryUpdateController.mProcessingLargeTask.set(false);
                 }
             }
         });
@@ -696,7 +706,7 @@
 
     // TODO: cache the file's existence so that we avoid doing a disk access each time.
     private boolean dictionaryFileExists() {
-        final File file = new File(mContext.getFilesDir(), mFilename);
+        final File file = getFileToOpenDict();
         return file.exists();
     }
 
@@ -711,7 +721,7 @@
             }
         };
         final Runnable oldTask = mUnfinishedFlushingTask.getAndSet(newTask);
-        getExecutor(mFilename).replaceAndExecute(oldTask, newTask);
+        getExecutor(mDictName).replaceAndExecute(oldTask, newTask);
     }
 
     /**
@@ -732,7 +742,7 @@
     @UsedForTesting
     public boolean isInDictionaryForTests(final String word) {
         final AsyncResultHolder<Boolean> holder = new AsyncResultHolder<Boolean>();
-        getExecutor(mFilename).executePrioritized(new Runnable() {
+        getExecutor(mDictName).executePrioritized(new Runnable() {
             @Override
             public void run() {
                 if (mDictType == Dictionary.TYPE_USER_HISTORY) {
@@ -745,24 +755,24 @@
 
     @UsedForTesting
     public void shutdownExecutorForTests() {
-        getExecutor(mFilename).shutdown();
+        getExecutor(mDictName).shutdown();
     }
 
     @UsedForTesting
     public boolean isTerminatedForTests() {
-        return getExecutor(mFilename).isTerminated();
+        return getExecutor(mDictName).isTerminated();
     }
 
     @UsedForTesting
     protected void runAfterGcForDebug(final Runnable r) {
-        getExecutor(mFilename).executePrioritized(new Runnable() {
+        getExecutor(mDictName).executePrioritized(new Runnable() {
             @Override
             public void run() {
                 try {
                     mBinaryDictionary.flushWithGC();
                     r.run();
                 } finally {
-                    mFilenameDictionaryUpdateController.mProcessingLargeTask.set(false);
+                    mDictNameDictionaryUpdateController.mProcessingLargeTask.set(false);
                 }
             }
         });
diff --git a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
index cc7687b..1dc3c54 100644
--- a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
@@ -18,7 +18,6 @@
 
 import android.content.ContentProviderClient;
 import android.content.ContentResolver;
-import android.content.ContentUris;
 import android.content.Context;
 import android.database.ContentObserver;
 import android.database.Cursor;
@@ -81,7 +80,7 @@
 
     public UserBinaryDictionary(final Context context, final Locale locale,
             final boolean alsoUseMoreRestrictiveLocales) {
-        super(context, getFilenameWithLocale(NAME, locale), locale, Dictionary.TYPE_USER,
+        super(context, getDictNameWithLocale(NAME, locale), locale, Dictionary.TYPE_USER,
                 false /* isUpdatable */);
         if (null == locale) throw new NullPointerException(); // Catch the error earlier
         final String localeStr = locale.toString();
diff --git a/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java b/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java
index 386e123..8321df9 100644
--- a/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java
+++ b/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java
@@ -58,13 +58,13 @@
     /** Locale for which this user history dictionary is storing words */
     private final Locale mLocale;
 
-    private final String mFileName;
+    private final String mDictName;
 
     /* package */ DecayingExpandableBinaryDictionaryBase(final Context context,
-            final Locale locale, final String dictionaryType, final String fileName) {
-        super(context, fileName, locale, dictionaryType, true);
+            final Locale locale, final String dictionaryType, final String dictName) {
+        super(context, dictName, locale, dictionaryType, true);
         mLocale = locale;
-        mFileName = fileName;
+        mDictName = dictName;
         if (mLocale != null && mLocale.toString().length() > 1) {
             reloadDictionaryIfRequired();
         }
@@ -88,7 +88,7 @@
                 FormatSpec.FileHeader.ATTRIBUTE_VALUE_TRUE);
         attributeMap.put(FormatSpec.FileHeader.HAS_HISTORICAL_INFO_ATTRIBUTE,
                 FormatSpec.FileHeader.ATTRIBUTE_VALUE_TRUE);
-        attributeMap.put(FormatSpec.FileHeader.DICTIONARY_ID_ATTRIBUTE, mFileName);
+        attributeMap.put(FormatSpec.FileHeader.DICTIONARY_ID_ATTRIBUTE, mDictName);
         attributeMap.put(FormatSpec.FileHeader.DICTIONARY_LOCALE_ATTRIBUTE, mLocale.toString());
         attributeMap.put(FormatSpec.FileHeader.DICTIONARY_VERSION_ATTRIBUTE,
                 String.valueOf(TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())));
@@ -113,9 +113,13 @@
     }
 
     @Override
-    protected String getFileNameExtensionToOpenDict() {
-        // TODO: pass the directory name instead
-        return "/" + FormatSpec.HEADER_FILE_EXTENSION;
+    protected String getFileNameToCreateDict(final String dictName) {
+        return dictName;
+    }
+
+    @Override
+    protected String getFileNameToOpenDict(final String dictName) {
+        return dictName + "/" + dictName + FormatSpec.HEADER_FILE_EXTENSION;
     }
 
     public void addMultipleDictionaryEntriesToDictionary(
@@ -194,7 +198,7 @@
         };
 
         // Load the dictionary from binary file
-        final File dictFile = new File(mContext.getFilesDir(), mFileName);
+        final File dictFile = new File(mContext.getFilesDir(), mDictName);
         final DictDecoder dictDecoder = FormatSpec.getDictDecoder(dictFile,
                 DictDecoder.USE_BYTEARRAY);
         if (dictDecoder == null) {
diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionary.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionary.java
index 413a951..b1ec76f 100644
--- a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionary.java
+++ b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionary.java
@@ -17,7 +17,6 @@
 package com.android.inputmethod.latin.personalization;
 
 import com.android.inputmethod.latin.Dictionary;
-import com.android.inputmethod.latin.ExpandableBinaryDictionary;
 import com.android.inputmethod.latin.utils.CollectionUtils;
 
 import java.util.ArrayList;
@@ -33,11 +32,7 @@
 
     /* package */ PersonalizationDictionary(final Context context, final Locale locale) {
         super(context, locale, Dictionary.TYPE_PERSONALIZATION,
-                getDictionaryFileName(locale.toString()));
-    }
-
-    private static String getDictionaryFileName(final String locale) {
-        return NAME + "." + locale + ExpandableBinaryDictionary.DICT_FILE_EXTENSION;
+                getDictNameWithLocale(NAME, locale));
     }
 
     public void registerUpdateSession(PersonalizationDictionaryUpdateSession session) {
diff --git a/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java b/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java
index 975224f..3f03de0 100644
--- a/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java
@@ -17,7 +17,6 @@
 package com.android.inputmethod.latin.personalization;
 
 import com.android.inputmethod.latin.Dictionary;
-import com.android.inputmethod.latin.ExpandableBinaryDictionary;
 
 import java.util.Locale;
 
@@ -31,12 +30,7 @@
     /* package for tests */ static final String NAME =
             UserHistoryDictionary.class.getSimpleName();
     /* package */ UserHistoryDictionary(final Context context, final Locale locale) {
-        super(context, locale, Dictionary.TYPE_USER_HISTORY,
-                getDictionaryFileName(locale.toString()));
-    }
-
-    private static String getDictionaryFileName(final String locale) {
-        return NAME + "." + locale + ExpandableBinaryDictionary.DICT_FILE_EXTENSION;
+        super(context, locale, Dictionary.TYPE_USER_HISTORY, getDictNameWithLocale(NAME, locale));
     }
 
     public void cancelAddingUserHistory(final String word0, final String word1) {
diff --git a/java/src/com/android/inputmethod/latin/utils/JsonUtils.java b/java/src/com/android/inputmethod/latin/utils/JsonUtils.java
new file mode 100644
index 0000000..764ef72
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/JsonUtils.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * 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.utils;
+
+import android.util.JsonReader;
+import android.util.JsonWriter;
+import android.util.Log;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public final class JsonUtils {
+    private static final String TAG = JsonUtils.class.getSimpleName();
+
+    private static final String INTEGER_CLASS_NAME = Integer.class.getSimpleName();
+    private static final String STRING_CLASS_NAME = String.class.getSimpleName();
+
+    private static final String EMPTY_STRING = "";
+
+    public static List<Object> jsonStrToList(final String s) {
+        final ArrayList<Object> list = CollectionUtils.newArrayList();
+        final JsonReader reader = new JsonReader(new StringReader(s));
+        try {
+            reader.beginArray();
+            while (reader.hasNext()) {
+                reader.beginObject();
+                while (reader.hasNext()) {
+                    final String name = reader.nextName();
+                    if (name.equals(INTEGER_CLASS_NAME)) {
+                        list.add(reader.nextInt());
+                    } else if (name.equals(STRING_CLASS_NAME)) {
+                        list.add(reader.nextString());
+                    } else {
+                        Log.w(TAG, "Invalid name: " + name);
+                        reader.skipValue();
+                    }
+                }
+                reader.endObject();
+            }
+            reader.endArray();
+            return list;
+        } catch (final IOException e) {
+        } finally {
+            close(reader);
+        }
+        return Collections.<Object>emptyList();
+    }
+
+    public static String listToJsonStr(final List<Object> list) {
+        if (list == null || list.isEmpty()) {
+            return EMPTY_STRING;
+        }
+        final StringWriter sw = new StringWriter();
+        final JsonWriter writer = new JsonWriter(sw);
+        try {
+            writer.beginArray();
+            for (final Object o : list) {
+                writer.beginObject();
+                if (o instanceof Integer) {
+                    writer.name(INTEGER_CLASS_NAME).value((Integer)o);
+                } else if (o instanceof String) {
+                    writer.name(STRING_CLASS_NAME).value((String)o);
+                }
+                writer.endObject();
+            }
+            writer.endArray();
+            return sw.toString();
+        } catch (final IOException e) {
+        } finally {
+            close(writer);
+        }
+        return EMPTY_STRING;
+    }
+
+    private static void close(final Closeable closeable) {
+        try {
+            if (closeable != null) {
+                closeable.close();
+            }
+        } catch (final IOException e) {
+            // Ignore
+        }
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/StringUtils.java b/java/src/com/android/inputmethod/latin/utils/StringUtils.java
index 928000e..df42041 100644
--- a/java/src/com/android/inputmethod/latin/utils/StringUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/StringUtils.java
@@ -17,20 +17,14 @@
 package com.android.inputmethod.latin.utils;
 
 import android.text.TextUtils;
-import android.util.JsonReader;
-import android.util.JsonWriter;
 import android.util.Log;
 
 import com.android.inputmethod.annotations.UsedForTesting;
 import com.android.inputmethod.latin.Constants;
 import com.android.inputmethod.latin.settings.SettingsValues;
 
-import java.io.Closeable;
 import java.io.IOException;
-import java.io.StringReader;
-import java.io.StringWriter;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 import java.util.Locale;
 
@@ -426,72 +420,4 @@
         }
         return bytes;
     }
-
-    private static final String INTEGER_CLASS_NAME = Integer.class.getSimpleName();
-    private static final String STRING_CLASS_NAME = String.class.getSimpleName();
-
-    public static List<Object> jsonStrToList(final String s) {
-        final ArrayList<Object> list = CollectionUtils.newArrayList();
-        final JsonReader reader = new JsonReader(new StringReader(s));
-        try {
-            reader.beginArray();
-            while (reader.hasNext()) {
-                reader.beginObject();
-                while (reader.hasNext()) {
-                    final String name = reader.nextName();
-                    if (name.equals(INTEGER_CLASS_NAME)) {
-                        list.add(reader.nextInt());
-                    } else if (name.equals(STRING_CLASS_NAME)) {
-                        list.add(reader.nextString());
-                    } else {
-                        Log.w(TAG, "Invalid name: " + name);
-                        reader.skipValue();
-                    }
-                }
-                reader.endObject();
-            }
-            reader.endArray();
-            return list;
-        } catch (final IOException e) {
-        } finally {
-            close(reader);
-        }
-        return Collections.<Object>emptyList();
-    }
-
-    public static String listToJsonStr(final List<Object> list) {
-        if (list == null || list.isEmpty()) {
-            return EMPTY_STRING;
-        }
-        final StringWriter sw = new StringWriter();
-        final JsonWriter writer = new JsonWriter(sw);
-        try {
-            writer.beginArray();
-            for (final Object o : list) {
-                writer.beginObject();
-                if (o instanceof Integer) {
-                    writer.name(INTEGER_CLASS_NAME).value((Integer)o);
-                } else if (o instanceof String) {
-                    writer.name(STRING_CLASS_NAME).value((String)o);
-                }
-                writer.endObject();
-            }
-            writer.endArray();
-            return sw.toString();
-        } catch (final IOException e) {
-        } finally {
-            close(writer);
-        }
-        return EMPTY_STRING;
-    }
-
-    private static void close(final Closeable closeable) {
-        try {
-            if (closeable != null) {
-                closeable.close();
-            }
-        } catch (final IOException e) {
-            // Ignore
-        }
-    }
 }
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/bigram_dict_content.h b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/bigram_dict_content.h
index ac05b21..95ee74f 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/bigram_dict_content.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/bigram_dict_content.h
@@ -73,8 +73,8 @@
 
     bool copyBigramList(const int bigramListPos, const int toPos);
 
-    bool flushToFile(const char *const dictDirPath) const {
-        return flush(dictDirPath, Ver4DictConstants::BIGRAM_LOOKUP_TABLE_FILE_EXTENSION,
+    bool flushToFile(const char *const dictBasePath) const {
+        return flush(dictBasePath, Ver4DictConstants::BIGRAM_LOOKUP_TABLE_FILE_EXTENSION,
                 Ver4DictConstants::BIGRAM_CONTENT_TABLE_FILE_EXTENSION,
                 Ver4DictConstants::BIGRAM_FILE_EXTENSION);
     }
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/probability_dict_content.cpp b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/probability_dict_content.cpp
index 01e406b..749e3fe 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/probability_dict_content.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/probability_dict_content.cpp
@@ -71,7 +71,7 @@
     return writeEntry(probabilityEntry, entryPos);
 }
 
-bool ProbabilityDictContent::flushToFile(const char *const dictDirPath) const {
+bool ProbabilityDictContent::flushToFile(const char *const dictBasePath) const {
     if (getEntryPos(mSize) < getBuffer()->getTailPosition()) {
         ProbabilityDictContent probabilityDictContentToWrite(mHasHistoricalInfo);
         for (int i = 0; i < mSize; ++i) {
@@ -81,10 +81,10 @@
                 return false;
             }
         }
-        return probabilityDictContentToWrite.flush(dictDirPath,
+        return probabilityDictContentToWrite.flush(dictBasePath,
                 Ver4DictConstants::FREQ_FILE_EXTENSION);
     } else {
-        return flush(dictDirPath, Ver4DictConstants::FREQ_FILE_EXTENSION);
+        return flush(dictBasePath, Ver4DictConstants::FREQ_FILE_EXTENSION);
     }
 }
 
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/shortcut_dict_content.cpp b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/shortcut_dict_content.cpp
index eca69ec..5552178 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/shortcut_dict_content.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/shortcut_dict_content.cpp
@@ -46,8 +46,8 @@
     return addressLookupTable->get(terminalId);
 }
 
-bool ShortcutDictContent::flushToFile(const char *const dictDirPath) const {
-    return flush(dictDirPath, Ver4DictConstants::SHORTCUT_LOOKUP_TABLE_FILE_EXTENSION,
+bool ShortcutDictContent::flushToFile(const char *const dictBasePath) const {
+    return flush(dictBasePath, Ver4DictConstants::SHORTCUT_LOOKUP_TABLE_FILE_EXTENSION,
             Ver4DictConstants::SHORTCUT_CONTENT_TABLE_FILE_EXTENSION,
             Ver4DictConstants::SHORTCUT_FILE_EXTENSION);
 }
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/shortcut_dict_content.h b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/shortcut_dict_content.h
index 670e6ea..a52214c 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/shortcut_dict_content.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/shortcut_dict_content.h
@@ -53,7 +53,7 @@
    // Returns head position of shortcut list for a PtNode specified by terminalId.
    int getShortcutListHeadPos(const int terminalId) const;
 
-   bool flushToFile(const char *const dictDirPath) const;
+   bool flushToFile(const char *const dictBasePath) const;
 
    bool runGC(const TerminalPositionLookupTable::TerminalIdMap *const terminalIdMap,
            const ShortcutDictContent *const originalShortcutDictContent);
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/single_dict_content.h b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/single_dict_content.h
index 9512bdb..d8eedf3 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/single_dict_content.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/single_dict_content.h
@@ -59,9 +59,9 @@
         return &mExpandableContentBuffer;
     }
 
-    bool flush(const char *const dictDirPath, const char *const contentFileName) const {
-        return DictFileWritingUtils::flushBufferToFileInDir(dictDirPath, contentFileName,
-                &mExpandableContentBuffer);
+    bool flush(const char *const dictBasePath, const char *const contentFileNameSuffix) const {
+        return DictFileWritingUtils::flushBufferToFileWithSuffix(dictBasePath,
+                contentFileNameSuffix, &mExpandableContentBuffer);
     }
 
  private:
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/sparse_table_dict_content.cpp b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/sparse_table_dict_content.cpp
index 84aceef..abb7d5f 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/sparse_table_dict_content.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/sparse_table_dict_content.cpp
@@ -18,18 +18,18 @@
 
 namespace latinime {
 
-bool SparseTableDictContent::flush(const char *const dictDirPath,
-        const char *const lookupTableFileName, const char *const addressTableFileName,
-        const char *const contentFileName) const {
-    if (!DictFileWritingUtils::flushBufferToFileInDir(dictDirPath, lookupTableFileName,
+bool SparseTableDictContent::flush(const char *const dictBasePath,
+        const char *const lookupTableFileNameSuffix, const char *const addressTableFileNameSuffix,
+        const char *const contentFileNameSuffix) const {
+    if (!DictFileWritingUtils::flushBufferToFileWithSuffix(dictBasePath, lookupTableFileNameSuffix,
             &mExpandableLookupTableBuffer)){
         return false;
     }
-    if (!DictFileWritingUtils::flushBufferToFileInDir(dictDirPath, addressTableFileName,
+    if (!DictFileWritingUtils::flushBufferToFileWithSuffix(dictBasePath, addressTableFileNameSuffix,
             &mExpandableAddressTableBuffer)) {
         return false;
     }
-    if (!DictFileWritingUtils::flushBufferToFileInDir(dictDirPath, contentFileName,
+    if (!DictFileWritingUtils::flushBufferToFileWithSuffix(dictBasePath, contentFileNameSuffix,
             &mExpandableContentBuffer)) {
         return false;
     }
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/terminal_position_lookup_table.cpp b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/terminal_position_lookup_table.cpp
index 24f62cd..c889cf5 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/terminal_position_lookup_table.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/terminal_position_lookup_table.cpp
@@ -50,7 +50,7 @@
             Ver4DictConstants::TERMINAL_ADDRESS_TABLE_ADDRESS_SIZE, getEntryPos(terminalId));
 }
 
-bool TerminalPositionLookupTable::flushToFile(const char *const dictDirPath) const {
+bool TerminalPositionLookupTable::flushToFile(const char *const dictBasePath) const {
     // If the used buffer size is smaller than the actual buffer size, regenerate the lookup
     // table and write the new table to the file.
     if (getEntryPos(mSize) < getBuffer()->getTailPosition()) {
@@ -63,12 +63,12 @@
                 return false;
             }
         }
-        return lookupTableToWrite.flush(dictDirPath,
+        return lookupTableToWrite.flush(dictBasePath,
                 Ver4DictConstants::TERMINAL_ADDRESS_TABLE_FILE_EXTENSION);
     } else {
         // We can simply use this lookup table because the buffer size has not been
         // changed.
-        return flush(dictDirPath, Ver4DictConstants::TERMINAL_ADDRESS_TABLE_FILE_EXTENSION);
+        return flush(dictBasePath, Ver4DictConstants::TERMINAL_ADDRESS_TABLE_FILE_EXTENSION);
     }
 }
 
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/terminal_position_lookup_table.h b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/terminal_position_lookup_table.h
index 283b402..5a28f52 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/terminal_position_lookup_table.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/terminal_position_lookup_table.h
@@ -44,7 +44,7 @@
         return mSize;
     }
 
-    bool flushToFile(const char *const dictDirPath) const;
+    bool flushToFile(const char *const dictBasePath) const;
 
     bool runGCTerminalIds(TerminalIdMap *const terminalIdMap);
 
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_dict_buffers.cpp b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_dict_buffers.cpp
index d17d2d5..e235540 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_dict_buffers.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_dict_buffers.cpp
@@ -17,6 +17,7 @@
 #include "suggest/policyimpl/dictionary/structure/v4/ver4_dict_buffers.h"
 
 #include <cerrno>
+#include <cstring>
 #include <sys/stat.h>
 #include <sys/types.h>
 
@@ -52,34 +53,42 @@
         AKLOGE("Cannot create directory: %s. errno: %d.", tmpDirPath, errno);
         return false;
     }
+    // Get dictionary base path.
+    const int dictNameBufSize = strlen(dictDirPath) + 1 /* terminator */;
+    char dictName[dictNameBufSize];
+    FileUtils::getBasename(dictDirPath, dictNameBufSize, dictName);
+    const int dictBasePathBufSize = FileUtils::getFilePathBufSize(tmpDirPath, dictName);
+    char dictBasePath[dictBasePathBufSize];
+    FileUtils::getFilePath(tmpDirPath, dictName, dictBasePathBufSize, dictBasePath);
+
     // Write header file.
-    if (!DictFileWritingUtils::flushBufferToFileInDir(tmpDirPath,
+    if (!DictFileWritingUtils::flushBufferToFileWithSuffix(dictBasePath,
             Ver4DictConstants::HEADER_FILE_EXTENSION, headerBuffer)) {
-        AKLOGE("Dictionary header file %s/%s cannot be written.", tmpDirPath,
+        AKLOGE("Dictionary header file %s%s cannot be written.", tmpDirPath,
                 Ver4DictConstants::HEADER_FILE_EXTENSION);
         return false;
     }
     // Write trie file.
-    if (!DictFileWritingUtils::flushBufferToFileInDir(tmpDirPath,
+    if (!DictFileWritingUtils::flushBufferToFileWithSuffix(dictBasePath,
             Ver4DictConstants::TRIE_FILE_EXTENSION, &mExpandableTrieBuffer)) {
-        AKLOGE("Dictionary trie file %s/%s cannot be written.", tmpDirPath,
+        AKLOGE("Dictionary trie file %s%s cannot be written.", tmpDirPath,
                 Ver4DictConstants::TRIE_FILE_EXTENSION);
         return false;
     }
     // Write dictionary contents.
-    if (!mTerminalPositionLookupTable.flushToFile(tmpDirPath)) {
+    if (!mTerminalPositionLookupTable.flushToFile(dictBasePath)) {
         AKLOGE("Terminal position lookup table cannot be written. %s", tmpDirPath);
         return false;
     }
-    if (!mProbabilityDictContent.flushToFile(tmpDirPath)) {
+    if (!mProbabilityDictContent.flushToFile(dictBasePath)) {
         AKLOGE("Probability dict content cannot be written. %s", tmpDirPath);
         return false;
     }
-    if (!mBigramDictContent.flushToFile(tmpDirPath)) {
+    if (!mBigramDictContent.flushToFile(dictBasePath)) {
         AKLOGE("Bigram dict content cannot be written. %s", tmpDirPath);
         return false;
     }
-    if (!mShortcutDictContent.flushToFile(tmpDirPath)) {
+    if (!mShortcutDictContent.flushToFile(dictBasePath)) {
         AKLOGE("Shortcut dict content cannot be written. %s", tmpDirPath);
         return false;
     }
diff --git a/native/jni/src/suggest/policyimpl/dictionary/utils/dict_file_writing_utils.cpp b/native/jni/src/suggest/policyimpl/dictionary/utils/dict_file_writing_utils.cpp
index ec67c18..442373b 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/utils/dict_file_writing_utils.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/utils/dict_file_writing_utils.cpp
@@ -83,11 +83,11 @@
     return true;
 }
 
-/* static */ bool DictFileWritingUtils::flushBufferToFileInDir(const char *const dirPath,
-        const char *const fileName, const BufferWithExtendableBuffer *const buffer) {
-    const int filePathBufSize = FileUtils::getFilePathBufSize(dirPath, fileName);
+/* static */ bool DictFileWritingUtils::flushBufferToFileWithSuffix(const char *const basePath,
+        const char *const suffix, const BufferWithExtendableBuffer *const buffer) {
+    const int filePathBufSize = FileUtils::getFilePathWithSuffixBufSize(basePath, suffix);
     char filePath[filePathBufSize];
-    FileUtils::getFilePath(dirPath, fileName, filePathBufSize, filePath);
+    FileUtils::getFilePathWithSuffix(basePath, suffix, filePathBufSize, filePath);
     return flushBufferToFile(filePath, buffer);
 }
 
diff --git a/native/jni/src/suggest/policyimpl/dictionary/utils/dict_file_writing_utils.h b/native/jni/src/suggest/policyimpl/dictionary/utils/dict_file_writing_utils.h
index ffd9db6..bdf9fd6 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/utils/dict_file_writing_utils.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/utils/dict_file_writing_utils.h
@@ -37,7 +37,7 @@
             BufferWithExtendableBuffer *const dictHeader,
             BufferWithExtendableBuffer *const dictBody);
 
-    static bool flushBufferToFileInDir(const char *const dirPath, const char *const fileName,
+    static bool flushBufferToFileWithSuffix(const char *const basePath, const char *const suffix,
             const BufferWithExtendableBuffer *const buffer);
 
  private:
diff --git a/native/jni/src/suggest/policyimpl/dictionary/utils/file_utils.cpp b/native/jni/src/suggest/policyimpl/dictionary/utils/file_utils.cpp
index 34da769..49ae7f1 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/utils/file_utils.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/utils/file_utils.cpp
@@ -20,6 +20,7 @@
 #include <cstring>
 #include <dirent.h>
 #include <fcntl.h>
+#include <libgen.h>
 #include <sys/types.h>
 #include <sys/stat.h>
 #include <unistd.h>
@@ -138,4 +139,19 @@
     }
 }
 
+/* static */ void FileUtils::getBasename(const char *const filePath,
+        const int outNameBufSize, char *const outName) {
+    const int filePathBufSize = strlen(filePath) + 1 /* terminator */;
+    char filePathBuf[filePathBufSize];
+    snprintf(filePathBuf, filePathBufSize, "%s", filePath);
+    const char *const baseName = basename(filePathBuf);
+    const int baseNameLength = strlen(baseName);
+    if (baseNameLength >= outNameBufSize) {
+        AKLOGE("outNameBufSize is too small. dirPath: %s, outNameBufSize: %d",
+                filePath, outNameBufSize);
+        return;
+    }
+    snprintf(outName, baseNameLength + 1 /* terminator */, "%s", baseName);
+}
+
 } // namespace latinime
diff --git a/native/jni/src/suggest/policyimpl/dictionary/utils/file_utils.h b/native/jni/src/suggest/policyimpl/dictionary/utils/file_utils.h
index e558373..3e84a303 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/utils/file_utils.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/utils/file_utils.h
@@ -48,6 +48,9 @@
     static void getDirPath(const char *const filePath, const int dirPathBufSize,
             char *const outDirPath);
 
+    static void getBasename(const char *const filePath, const int outNameBufSize,
+            char *const outName);
+
  private:
     DISALLOW_IMPLICIT_CONSTRUCTORS(FileUtils);
 };
diff --git a/tests/src/com/android/inputmethod/latin/BinaryDictionaryDecayingTests.java b/tests/src/com/android/inputmethod/latin/BinaryDictionaryDecayingTests.java
index d76a552..931ba7d 100644
--- a/tests/src/com/android/inputmethod/latin/BinaryDictionaryDecayingTests.java
+++ b/tests/src/com/android/inputmethod/latin/BinaryDictionaryDecayingTests.java
@@ -122,12 +122,13 @@
                 FormatSpec.FileHeader.ATTRIBUTE_VALUE_TRUE);
         attributeMap.put(FormatSpec.FileHeader.HAS_HISTORICAL_INFO_ATTRIBUTE,
                 FormatSpec.FileHeader.ATTRIBUTE_VALUE_TRUE);
+        final String headerFileName = file.getName() + FormatSpec.HEADER_FILE_EXTENSION;
         if (BinaryDictionary.createEmptyDictFile(file.getAbsolutePath(),
                 FormatSpec.VERSION4, attributeMap)) {
-            return new File(file, FormatSpec.HEADER_FILE_EXTENSION);
+            return new File(file, headerFileName);
         } else {
             throw new IOException("Empty dictionary " + file.getAbsolutePath() + " "
-                    + FormatSpec.HEADER_FILE_EXTENSION + " cannot be created.");
+                    + headerFileName + " cannot be created.");
         }
     }
 
diff --git a/tests/src/com/android/inputmethod/latin/BinaryDictionaryTests.java b/tests/src/com/android/inputmethod/latin/BinaryDictionaryTests.java
index c148402..d9d4a55 100644
--- a/tests/src/com/android/inputmethod/latin/BinaryDictionaryTests.java
+++ b/tests/src/com/android/inputmethod/latin/BinaryDictionaryTests.java
@@ -70,12 +70,13 @@
         Map<String, String> attributeMap = new HashMap<String, String>();
         attributeMap.put(FormatSpec.FileHeader.SUPPORTS_DYNAMIC_UPDATE_ATTRIBUTE,
                 FormatSpec.FileHeader.ATTRIBUTE_VALUE_TRUE);
+        final String headerFileName = file.getName() + FormatSpec.HEADER_FILE_EXTENSION;
         if (BinaryDictionary.createEmptyDictFile(file.getAbsolutePath(),
                 FormatSpec.VERSION4, attributeMap)) {
-            return new File(file, FormatSpec.HEADER_FILE_EXTENSION);
+            return new File(file, headerFileName);
         } else {
             throw new IOException("Empty dictionary " + file.getAbsolutePath() + " "
-                    + FormatSpec.HEADER_FILE_EXTENSION + " cannot be created.");
+                    + headerFileName + " cannot be created.");
         }
     }
 
diff --git a/tests/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryTests.java b/tests/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryTests.java
index 717b04f..f0fe3f2 100644
--- a/tests/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryTests.java
+++ b/tests/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryTests.java
@@ -20,7 +20,6 @@
 import android.test.suitebuilder.annotation.LargeTest;
 import android.util.Log;
 
-import com.android.inputmethod.latin.ExpandableBinaryDictionary;
 import com.android.inputmethod.latin.utils.CollectionUtils;
 
 import java.io.File;
@@ -137,8 +136,7 @@
         Log.d(TAG, "This test can be used for profiling.");
         Log.d(TAG, "Usage: please set UserHistoryDictionary.PROFILE_SAVE_RESTORE to true.");
         final String testFilenameSuffix = "test_random_words" + System.currentTimeMillis();
-        final String fileName = UserHistoryDictionary.NAME + "." + testFilenameSuffix
-                + ExpandableBinaryDictionary.DICT_FILE_EXTENSION;
+        final String fileName = UserHistoryDictionary.NAME + "." + testFilenameSuffix;
 
         final int numberOfWords = 1000;
         final Random random = new Random(123456);
@@ -172,8 +170,7 @@
             // Create filename suffixes for this test.
             for (int i = 0; i < numberOfLanguages; i++) {
                 testFilenameSuffixes[i] = "test_switching_languages" + i;
-                final String fileName = UserHistoryDictionary.NAME + "." +
-                        testFilenameSuffixes[i] + ExpandableBinaryDictionary.DICT_FILE_EXTENSION;
+                final String fileName = UserHistoryDictionary.NAME + "." + testFilenameSuffixes[i];
                 dictFiles[i] = new File(getContext().getFilesDir(), fileName);
                 clearHistory(testFilenameSuffixes[i]);
             }
@@ -217,8 +214,7 @@
         } finally {
             Log.d(TAG, "waiting for writing ...");
             waitForWriting(testFilenameSuffix);
-            final String fileName = UserHistoryDictionary.NAME + "." + testFilenameSuffix
-                    + ExpandableBinaryDictionary.DICT_FILE_EXTENSION;
+            final String fileName = UserHistoryDictionary.NAME + "." + testFilenameSuffix;
             final File dictFile = new File(getContext().getFilesDir(), fileName);
             if (dictFile != null) {
                 assertTrue(dictFile.exists());
diff --git a/tests/src/com/android/inputmethod/latin/utils/StringUtilsTests.java b/tests/src/com/android/inputmethod/latin/utils/StringAndJsonUtilsTests.java
similarity index 98%
rename from tests/src/com/android/inputmethod/latin/utils/StringUtilsTests.java
rename to tests/src/com/android/inputmethod/latin/utils/StringAndJsonUtilsTests.java
index 21fcf11..2123e84 100644
--- a/tests/src/com/android/inputmethod/latin/utils/StringUtilsTests.java
+++ b/tests/src/com/android/inputmethod/latin/utils/StringAndJsonUtilsTests.java
@@ -26,7 +26,7 @@
 import java.util.Locale;
 
 @SmallTest
-public class StringUtilsTests extends AndroidTestCase {
+public class StringAndJsonUtilsTests extends AndroidTestCase {
     public void testContainsInArray() {
         assertFalse("empty array", StringUtils.containsInArray("key", new String[0]));
         assertFalse("not in 1 element", StringUtils.containsInArray("key", new String[] {
@@ -292,11 +292,11 @@
         assertTrue(bytesStr.equals(bytesStr2));
     }
 
-    public void testJsonStringUtils() {
+    public void testJsonUtils() {
         final Object[] objs = new Object[] { 1, "aaa", "bbb", 3 };
         final List<Object> objArray = Arrays.asList(objs);
-        final String str = StringUtils.listToJsonStr(objArray);
-        final List<Object> newObjArray = StringUtils.jsonStrToList(str);
+        final String str = JsonUtils.listToJsonStr(objArray);
+        final List<Object> newObjArray = JsonUtils.jsonStrToList(str);
         for (int i = 0; i < objs.length; ++i) {
             assertEquals(objs[i], newObjArray.get(i));
         }
diff --git a/tools/make-keyboard-text/res/values-es/donottranslate-more-keys.xml b/tools/make-keyboard-text/res/values-es/donottranslate-more-keys.xml
index deae879..f4fe7f7 100644
--- a/tools/make-keyboard-text/res/values-es/donottranslate-more-keys.xml
+++ b/tools/make-keyboard-text/res/values-es/donottranslate-more-keys.xml
@@ -67,8 +67,6 @@
          U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE
          U+010D: "č" LATIN SMALL LETTER C WITH CARON -->
     <string name="more_keys_for_c">&#x00E7;,&#x0107;,&#x010D;</string>
-    <!-- U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE -->
-    <string name="keylabel_for_spanish_row2_10">&#x00F1;</string>
     <!-- U+00A1: "¡" INVERTED EXCLAMATION MARK
          U+00BF: "¿" INVERTED QUESTION MARK -->
     <string name="more_keys_for_punctuation">"!fixedColumnOrder!9,&#x00A1;,;,/,(,),#,!,\\,,\?,&#x00BF;,&amp;,\\%,+,\",-,:,',\@"</string>
diff --git a/tools/make-keyboard-text/res/values/donottranslate-more-keys.xml b/tools/make-keyboard-text/res/values/donottranslate-more-keys.xml
index c1f3e4c..ceb46dc 100644
--- a/tools/make-keyboard-text/res/values/donottranslate-more-keys.xml
+++ b/tools/make-keyboard-text/res/values/donottranslate-more-keys.xml
@@ -189,7 +189,8 @@
     <string name="keylabel_for_w">w</string>
     <string name="keylabel_for_y">y</string>
     <string name="keylabel_for_x">x</string>
-    <string name="keylabel_for_spanish_row2_10"></string>
+    <!-- U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE -->
+    <string name="keylabel_for_spanish_row2_10">&#x00F1;</string>
     <string name="more_keys_for_am_pm">!fixedColumnOrder!2,!hasLabels!,\@string/label_time_am,\@string/label_time_pm</string>
     <string name="settings_as_more_key">!icon/settings_key|!code/key_settings</string>
     <string name="shortcut_as_more_key">!icon/shortcut_key|!code/key_shortcut</string>