Merge "Move children filtering methods to DicNodeChildrenFilter."
diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionaryService.java b/java/src/com/android/inputmethod/dictionarypack/DictionaryService.java
index 6e3dd71..3b00723 100644
--- a/java/src/com/android/inputmethod/dictionarypack/DictionaryService.java
+++ b/java/src/com/android/inputmethod/dictionarypack/DictionaryService.java
@@ -170,7 +170,7 @@
             checkTimeAndMaybeSetupUpdateAlarm(context);
         } else if (DictionaryPackConstants.UPDATE_NOW_INTENT_ACTION.equals(intent.getAction())) {
             // Intent to trigger an update now.
-            UpdateHandler.update(context, false);
+            UpdateHandler.tryUpdate(context, false);
         } else {
             UpdateHandler.downloadFinished(context, intent);
         }
@@ -221,7 +221,7 @@
      */
     public static void updateNowIfNotUpdatedInAVeryLongTime(final Context context) {
         if (!isLastUpdateAtLeastThisOld(context, VERY_LONG_TIME)) return;
-        UpdateHandler.update(context, false);
+        UpdateHandler.tryUpdate(context, false);
     }
 
     /**
diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java b/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java
index 4b89d20..7bbd041 100644
--- a/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java
+++ b/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java
@@ -30,6 +30,7 @@
 import android.preference.Preference;
 import android.preference.PreferenceFragment;
 import android.preference.PreferenceGroup;
+import android.text.TextUtils;
 import android.text.format.DateUtils;
 import android.util.Log;
 import android.view.animation.AnimationUtils;
@@ -104,9 +105,16 @@
 
     @Override
     public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
-        mUpdateNowMenu = menu.add(Menu.NONE, MENU_UPDATE_NOW, 0, R.string.check_for_updates_now);
-        mUpdateNowMenu.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
-        refreshNetworkState();
+        final String metadataUri =
+                MetadataDbHelper.getMetadataUriAsString(getActivity(), mClientId);
+        // We only add the "Refresh" button if we have a non-empty URL to refresh from. If the
+        // URL is empty, of course we can't refresh so it makes no sense to display this.
+        if (!TextUtils.isEmpty(metadataUri)) {
+            mUpdateNowMenu =
+                    menu.add(Menu.NONE, MENU_UPDATE_NOW, 0, R.string.check_for_updates_now);
+            mUpdateNowMenu.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+            refreshNetworkState();
+        }
     }
 
     @Override
@@ -353,7 +361,12 @@
         new Thread("updateByHand") {
             @Override
             public void run() {
-                UpdateHandler.update(activity, true);
+                // We call tryUpdate(), which returns whether we could successfully start an update.
+                // If we couldn't, we'll never receive the end callback, so we stop the loading
+                // animation and return to the previous screen.
+                if (!UpdateHandler.tryUpdate(activity, true)) {
+                    stopLoadingAnimation();
+                }
             }
         }.start();
     }
@@ -368,7 +381,9 @@
     private void startLoadingAnimation() {
         mLoadingView.setVisibility(View.VISIBLE);
         getView().setVisibility(View.GONE);
-        mUpdateNowMenu.setTitle(R.string.cancel);
+        // We come here when the menu element is pressed so presumably it can't be null. But
+        // better safe than sorry.
+        if (null != mUpdateNowMenu) mUpdateNowMenu.setTitle(R.string.cancel);
     }
 
     private void stopLoadingAnimation() {
diff --git a/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java b/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java
index 719f24e..8a23acd 100644
--- a/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java
+++ b/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java
@@ -173,14 +173,15 @@
      * Download latest metadata from the server through DownloadManager for all known clients
      * @param context The context for retrieving resources
      * @param updateNow Whether we should update NOW, or respect bandwidth policies
+     * @return true if an update successfully started, false otherwise.
      */
-    public static void update(final Context context, final boolean updateNow) {
+    public static boolean tryUpdate(final Context context, final boolean updateNow) {
         // TODO: loop through all clients instead of only doing the default one.
         final TreeSet<String> uris = new TreeSet<String>();
         final Cursor cursor = MetadataDbHelper.queryClientIds(context);
-        if (null == cursor) return;
+        if (null == cursor) return false;
         try {
-            if (!cursor.moveToFirst()) return;
+            if (!cursor.moveToFirst()) return false;
             do {
                 final String clientId = cursor.getString(0);
                 final String metadataUri =
@@ -192,6 +193,7 @@
         } finally {
             cursor.close();
         }
+        boolean started = false;
         for (final String metadataUri : uris) {
             if (!TextUtils.isEmpty(metadataUri)) {
                 // If the metadata URI is empty, that means we should never update it at all.
@@ -200,8 +202,10 @@
                 // is a bug and it happens anyway, doing nothing is the right thing to do.
                 // For more information, {@see DictionaryProvider#insert(Uri, ContentValues)}.
                 updateClientsWithMetadataUri(context, updateNow, metadataUri);
+                started = true;
             }
         }
+        return started;
     }
 
     /**
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
index 51dc852..31a892e 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
@@ -224,14 +224,10 @@
         }
     }
 
-    // ## HACK ## we prevent usage of a dictionary before version 18 for English only. The reason
-    // for this is, since those do not include whitelist entries, the new code with an old version
-    // of the dictionary would lose whitelist functionality.
+    // ## HACK ## we prevent usage of a dictionary before version 18. The reason for this is, since
+    // those do not include whitelist entries, the new code with an old version of the dictionary
+    // would lose whitelist functionality.
     private static boolean hackCanUseDictionaryFile(final Locale locale, final File f) {
-        // Only for English - other languages didn't have a whitelist, hence this
-        // ad-hoc ## HACK ##
-        if (!Locale.ENGLISH.getLanguage().equals(locale.getLanguage())) return true;
-
         FileInputStream inStream = null;
         try {
             // Read the version of the file
diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
index 9cdb86c..a19363d 100644
--- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
@@ -92,7 +92,8 @@
     /** Controls access to the local binary dictionary for this instance. */
     private final DictionaryController mLocalDictionaryController = new DictionaryController();
 
-    private static final int BINARY_DICT_VERSION = 1;
+    // TODO: Regenerate version 3 binary dictionary.
+    private static final int BINARY_DICT_VERSION = 2;
     private static final FormatSpec.FormatOptions FORMAT_OPTIONS =
             new FormatSpec.FormatOptions(BINARY_DICT_VERSION);
 
@@ -415,6 +416,12 @@
                 // shared dictionary.
                 loadBinaryDictionary();
             }
+            if (mBinaryDictionary != null && !mBinaryDictionary.isValidDictionary()) {
+                // Binary dictionary is not valid. Regenerate the dictionary file.
+                mSharedDictionaryController.mLastUpdateTime = time;
+                generateBinaryDictionary();
+                loadBinaryDictionary();
+            }
             mLocalDictionaryController.mLastUpdateTime = time;
         } finally {
             mSharedDictionaryController.unlock();
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 0560cf5..243928f 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -73,7 +73,6 @@
 import com.android.inputmethod.keyboard.KeyboardId;
 import com.android.inputmethod.keyboard.KeyboardSwitcher;
 import com.android.inputmethod.keyboard.MainKeyboardView;
-import com.android.inputmethod.latin.RichInputConnection.Range;
 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
 import com.android.inputmethod.latin.define.ProductionFlag;
 import com.android.inputmethod.latin.suggestions.SuggestionStripView;
@@ -87,6 +86,7 @@
 import com.android.inputmethod.latin.utils.RecapitalizeStatus;
 import com.android.inputmethod.latin.utils.StaticInnerHandlerWrapper;
 import com.android.inputmethod.latin.utils.TargetPackageInfoGetterTask;
+import com.android.inputmethod.latin.utils.TextRange;
 import com.android.inputmethod.latin.utils.Utils;
 import com.android.inputmethod.latin.utils.Utils.Stats;
 import com.android.inputmethod.research.ResearchLogger;
@@ -961,7 +961,11 @@
             // TODO: is it still necessary to test for composingSpan related stuff?
             final boolean selectionChangedOrSafeToReset = selectionChanged
                     || (!mWordComposer.isComposingWord()) || noComposingSpan;
-            if (selectionChangedOrSafeToReset) {
+            final boolean hasOrHadSelection = (oldSelStart != oldSelEnd
+                    || newSelStart != newSelEnd);
+            final int moveAmount = newSelStart - oldSelStart;
+            if (selectionChangedOrSafeToReset && (hasOrHadSelection
+                    || !mWordComposer.moveCursorByAndReturnIfInsideComposingWord(moveAmount))) {
                 // If we are composing a word and moving the cursor, we would want to set a
                 // suggestion span for recorrection to work correctly. Unfortunately, that
                 // would involve the keyboard committing some new text, which would move the
@@ -2514,7 +2518,7 @@
         // If we don't know the cursor location, return.
         if (mLastSelectionStart < 0) return;
         if (!mConnection.isCursorTouchingWord(mSettings.getCurrent())) return;
-        final Range range = mConnection.getWordRangeAtCursor(mSettings.getWordSeparators(),
+        final TextRange range = mConnection.getWordRangeAtCursor(mSettings.getWordSeparators(),
                 0 /* additionalPrecedingWordsCount */);
         if (null == range) return; // Happens if we don't have an input connection at all
         // If for some strange reason (editor bug or so) we measure the text before the cursor as
@@ -2535,6 +2539,7 @@
             }
         }
         mWordComposer.setComposingWord(typedWord, mKeyboardSwitcher.getKeyboard());
+        // TODO: this is in chars but the callee expects code points!
         mWordComposer.setCursorPositionWithinWord(numberOfCharsInWordBeforeCursor);
         mConnection.setComposingRegion(
                 mLastSelectionStart - numberOfCharsInWordBeforeCursor,
diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java
index 6b22cb1..39170cf 100644
--- a/java/src/com/android/inputmethod/latin/RichInputConnection.java
+++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java
@@ -17,9 +17,7 @@
 package com.android.inputmethod.latin;
 
 import android.inputmethodservice.InputMethodService;
-import android.text.Spanned;
 import android.text.TextUtils;
-import android.text.style.SuggestionSpan;
 import android.util.Log;
 import android.view.KeyEvent;
 import android.view.inputmethod.CompletionInfo;
@@ -32,9 +30,9 @@
 import com.android.inputmethod.latin.utils.CapsModeUtils;
 import com.android.inputmethod.latin.utils.DebugLogUtils;
 import com.android.inputmethod.latin.utils.StringUtils;
+import com.android.inputmethod.latin.utils.TextRange;
 import com.android.inputmethod.research.ResearchLogger;
 
-import java.util.Arrays;
 import java.util.Locale;
 import java.util.regex.Pattern;
 
@@ -441,100 +439,6 @@
         return getNthPreviousWord(prev, sentenceSeperators, n);
     }
 
-    /**
-     * Represents a range of text, relative to the current cursor position.
-     */
-    public static final class Range {
-        private final CharSequence mTextAtCursor;
-        private final int mWordAtCursorStartIndex;
-        private final int mWordAtCursorEndIndex;
-        private final int mCursorIndex;
-
-        public final CharSequence mWord;
-
-        public int getNumberOfCharsInWordBeforeCursor() {
-            return mCursorIndex - mWordAtCursorStartIndex;
-        }
-
-        public int getNumberOfCharsInWordAfterCursor() {
-            return mWordAtCursorEndIndex - mCursorIndex;
-        }
-
-        /**
-         * Gets the suggestion spans that are put squarely on the word, with the exact start
-         * and end of the span matching the boundaries of the word.
-         * @return the list of spans.
-         */
-        public SuggestionSpan[] getSuggestionSpansAtWord() {
-            if (!(mTextAtCursor instanceof Spanned && mWord instanceof Spanned)) {
-                return new SuggestionSpan[0];
-            }
-            final Spanned text = (Spanned)mTextAtCursor;
-            // Note: it's fine to pass indices negative or greater than the length of the string
-            // to the #getSpans() method. The reason we need to get from -1 to +1 is that, the
-            // spans were cut at the cursor position, and #getSpans(start, end) does not return
-            // spans that end at `start' or begin at `end'. Consider the following case:
-            //              this| is          (The | symbolizes the cursor position
-            //              ---- ---
-            // In this case, the cursor is in position 4, so the 0~7 span has been split into
-            // a 0~4 part and a 4~7 part.
-            // If we called #getSpans(0, 4) in this case, we would only get the part from 0 to 4
-            // of the span, and not the part from 4 to 7, so we would not realize the span actually
-            // extends from 0 to 7. But if we call #getSpans(-1, 5) we'll get both the 0~4 and
-            // the 4~7 spans and we can merge them accordingly.
-            // Any span starting more than 1 char away from the word boundaries in any direction
-            // does not touch the word, so we don't need to consider it. That's why requesting
-            // -1 ~ +1 is enough.
-            // Of course this is only relevant if the cursor is at one end of the word. If it's
-            // in the middle, the -1 and +1 are not necessary, but they are harmless.
-            final SuggestionSpan[] spans = text.getSpans(mWordAtCursorStartIndex - 1,
-                    mWordAtCursorEndIndex + 1, SuggestionSpan.class);
-            int readIndex = 0;
-            int writeIndex = 0;
-            for (; readIndex < spans.length; ++readIndex) {
-                final SuggestionSpan span = spans[readIndex];
-                // The span may be null, as we null them when we find duplicates. Cf a few lines
-                // down.
-                if (null == span) continue;
-                // Tentative span start and end. This may be modified later if we realize the
-                // same span is also applied to other parts of the string.
-                int spanStart = text.getSpanStart(span);
-                int spanEnd = text.getSpanEnd(span);
-                for (int i = readIndex + 1; i < spans.length; ++i) {
-                    if (span.equals(spans[i])) {
-                        // We found the same span somewhere else. Read the new extent of this
-                        // span, and adjust our values accordingly.
-                        spanStart = Math.min(spanStart, text.getSpanStart(spans[i]));
-                        spanEnd = Math.max(spanEnd, text.getSpanEnd(spans[i]));
-                        // ...and mark the span as processed.
-                        spans[i] = null;
-                    }
-                }
-                if (spanStart == mWordAtCursorStartIndex && spanEnd == mWordAtCursorEndIndex) {
-                    // If the span does not start and stop here, we ignore it. It probably extends
-                    // past the start or end of the word, as happens in missing space correction
-                    // or EasyEditSpans put by voice input.
-                    spans[writeIndex++] = spans[readIndex];
-                }
-            }
-            return writeIndex == readIndex ? spans : Arrays.copyOfRange(spans, 0, writeIndex);
-        }
-
-        public Range(final CharSequence textAtCursor, final int wordAtCursorStartIndex,
-                final int wordAtCursorEndIndex, final int cursorIndex) {
-            if (wordAtCursorStartIndex < 0 || cursorIndex < wordAtCursorStartIndex
-                    || cursorIndex > wordAtCursorEndIndex
-                    || wordAtCursorEndIndex > textAtCursor.length()) {
-                throw new IndexOutOfBoundsException();
-            }
-            mTextAtCursor = textAtCursor;
-            mWordAtCursorStartIndex = wordAtCursorStartIndex;
-            mWordAtCursorEndIndex = wordAtCursorEndIndex;
-            mCursorIndex = cursorIndex;
-            mWord = mTextAtCursor.subSequence(mWordAtCursorStartIndex, mWordAtCursorEndIndex);
-        }
-    }
-
     private static boolean isSeparator(int code, String sep) {
         return sep.indexOf(code) != -1;
     }
@@ -581,7 +485,7 @@
      */
     public CharSequence getWordAtCursor(String separators) {
         // getWordRangeAtCursor returns null if the connection is null
-        Range r = getWordRangeAtCursor(separators, 0);
+        TextRange r = getWordRangeAtCursor(separators, 0);
         return (r == null) ? null : r.mWord;
     }
 
@@ -593,7 +497,8 @@
      *   be included in the returned range
      * @return a range containing the text surrounding the cursor
      */
-    public Range getWordRangeAtCursor(final String sep, final int additionalPrecedingWordsCount) {
+    public TextRange getWordRangeAtCursor(final String sep,
+            final int additionalPrecedingWordsCount) {
         mIC = mParent.getCurrentInputConnection();
         if (mIC == null || sep == null) {
             return null;
@@ -643,7 +548,7 @@
             }
         }
 
-        return new Range(TextUtils.concat(before, after), startIndexInBefore,
+        return new TextRange(TextUtils.concat(before, after), startIndexInBefore,
                 before.length() + endIndexInAfter, before.length());
     }
 
diff --git a/java/src/com/android/inputmethod/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java
index e078f03..2babe8b 100644
--- a/java/src/com/android/inputmethod/latin/WordComposer.java
+++ b/java/src/com/android/inputmethod/latin/WordComposer.java
@@ -192,6 +192,40 @@
         return mCursorPositionWithinWord != mCodePointSize;
     }
 
+    /**
+     * When the cursor is moved by the user, we need to update its position.
+     * If it falls inside the currently composing word, we don't reset the composition, and
+     * only update the cursor position.
+     *
+     * @param expectedMoveAmount How many java chars to move the cursor. Negative values move
+     * the cursor backward, positive values move the cursor forward.
+     * @return true if the cursor is still inside the composing word, false otherwise.
+     */
+    public boolean moveCursorByAndReturnIfInsideComposingWord(final int expectedMoveAmount) {
+        int actualMoveAmountWithinWord = 0;
+        int cursorPos = mCursorPositionWithinWord;
+        if (expectedMoveAmount >= 0) {
+            // Moving the cursor forward for the expected amount or until the end of the word has
+            // been reached, whichever comes first.
+            while (actualMoveAmountWithinWord < expectedMoveAmount && cursorPos < mCodePointSize) {
+                actualMoveAmountWithinWord += Character.charCount(mPrimaryKeyCodes[cursorPos]);
+                ++cursorPos;
+            }
+        } else {
+            // Moving the cursor backward for the expected amount or until the start of the word
+            // has been reached, whichever comes first.
+            while (actualMoveAmountWithinWord > expectedMoveAmount && cursorPos > 0) {
+                --cursorPos;
+                actualMoveAmountWithinWord -= Character.charCount(mPrimaryKeyCodes[cursorPos]);
+            }
+        }
+        // If the actual and expected amounts differ, we crossed the start or the end of the word
+        // so the result would not be inside the composing word.
+        if (actualMoveAmountWithinWord != expectedMoveAmount) return false;
+        mCursorPositionWithinWord = cursorPos;
+        return true;
+    }
+
     public void setBatchInputPointers(final InputPointers batchPointers) {
         mInputPointers.set(batchPointers);
         mIsBatchMode = true;
diff --git a/java/src/com/android/inputmethod/latin/utils/TextRange.java b/java/src/com/android/inputmethod/latin/utils/TextRange.java
new file mode 100644
index 0000000..5793e41
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/TextRange.java
@@ -0,0 +1,116 @@
+/*
+ * 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.text.Spanned;
+import android.text.style.SuggestionSpan;
+
+import java.util.Arrays;
+
+/**
+ * Represents a range of text, relative to the current cursor position.
+ */
+public final class TextRange {
+    private final CharSequence mTextAtCursor;
+    private final int mWordAtCursorStartIndex;
+    private final int mWordAtCursorEndIndex;
+    private final int mCursorIndex;
+
+    public final CharSequence mWord;
+
+    public int getNumberOfCharsInWordBeforeCursor() {
+        return mCursorIndex - mWordAtCursorStartIndex;
+    }
+
+    public int getNumberOfCharsInWordAfterCursor() {
+        return mWordAtCursorEndIndex - mCursorIndex;
+    }
+
+    /**
+     * Gets the suggestion spans that are put squarely on the word, with the exact start
+     * and end of the span matching the boundaries of the word.
+     * @return the list of spans.
+     */
+    public SuggestionSpan[] getSuggestionSpansAtWord() {
+        if (!(mTextAtCursor instanceof Spanned && mWord instanceof Spanned)) {
+            return new SuggestionSpan[0];
+        }
+        final Spanned text = (Spanned)mTextAtCursor;
+        // Note: it's fine to pass indices negative or greater than the length of the string
+        // to the #getSpans() method. The reason we need to get from -1 to +1 is that, the
+        // spans were cut at the cursor position, and #getSpans(start, end) does not return
+        // spans that end at `start' or begin at `end'. Consider the following case:
+        //              this| is          (The | symbolizes the cursor position
+        //              ---- ---
+        // In this case, the cursor is in position 4, so the 0~7 span has been split into
+        // a 0~4 part and a 4~7 part.
+        // If we called #getSpans(0, 4) in this case, we would only get the part from 0 to 4
+        // of the span, and not the part from 4 to 7, so we would not realize the span actually
+        // extends from 0 to 7. But if we call #getSpans(-1, 5) we'll get both the 0~4 and
+        // the 4~7 spans and we can merge them accordingly.
+        // Any span starting more than 1 char away from the word boundaries in any direction
+        // does not touch the word, so we don't need to consider it. That's why requesting
+        // -1 ~ +1 is enough.
+        // Of course this is only relevant if the cursor is at one end of the word. If it's
+        // in the middle, the -1 and +1 are not necessary, but they are harmless.
+        final SuggestionSpan[] spans = text.getSpans(mWordAtCursorStartIndex - 1,
+                mWordAtCursorEndIndex + 1, SuggestionSpan.class);
+        int readIndex = 0;
+        int writeIndex = 0;
+        for (; readIndex < spans.length; ++readIndex) {
+            final SuggestionSpan span = spans[readIndex];
+            // The span may be null, as we null them when we find duplicates. Cf a few lines
+            // down.
+            if (null == span) continue;
+            // Tentative span start and end. This may be modified later if we realize the
+            // same span is also applied to other parts of the string.
+            int spanStart = text.getSpanStart(span);
+            int spanEnd = text.getSpanEnd(span);
+            for (int i = readIndex + 1; i < spans.length; ++i) {
+                if (span.equals(spans[i])) {
+                    // We found the same span somewhere else. Read the new extent of this
+                    // span, and adjust our values accordingly.
+                    spanStart = Math.min(spanStart, text.getSpanStart(spans[i]));
+                    spanEnd = Math.max(spanEnd, text.getSpanEnd(spans[i]));
+                    // ...and mark the span as processed.
+                    spans[i] = null;
+                }
+            }
+            if (spanStart == mWordAtCursorStartIndex && spanEnd == mWordAtCursorEndIndex) {
+                // If the span does not start and stop here, we ignore it. It probably extends
+                // past the start or end of the word, as happens in missing space correction
+                // or EasyEditSpans put by voice input.
+                spans[writeIndex++] = spans[readIndex];
+            }
+        }
+        return writeIndex == readIndex ? spans : Arrays.copyOfRange(spans, 0, writeIndex);
+    }
+
+    public TextRange(final CharSequence textAtCursor, final int wordAtCursorStartIndex,
+            final int wordAtCursorEndIndex, final int cursorIndex) {
+        if (wordAtCursorStartIndex < 0 || cursorIndex < wordAtCursorStartIndex
+                || cursorIndex > wordAtCursorEndIndex
+                || wordAtCursorEndIndex > textAtCursor.length()) {
+            throw new IndexOutOfBoundsException();
+        }
+        mTextAtCursor = textAtCursor;
+        mWordAtCursorStartIndex = wordAtCursorStartIndex;
+        mWordAtCursorEndIndex = wordAtCursorEndIndex;
+        mCursorIndex = cursorIndex;
+        mWord = mTextAtCursor.subSequence(mWordAtCursorStartIndex, mWordAtCursorEndIndex);
+    }
+}
\ No newline at end of file
diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java
index f073308..06a21bc 100644
--- a/java/src/com/android/inputmethod/research/ResearchLogger.java
+++ b/java/src/com/android/inputmethod/research/ResearchLogger.java
@@ -57,11 +57,11 @@
 import com.android.inputmethod.latin.LatinIME;
 import com.android.inputmethod.latin.R;
 import com.android.inputmethod.latin.RichInputConnection;
-import com.android.inputmethod.latin.RichInputConnection.Range;
 import com.android.inputmethod.latin.Suggest;
 import com.android.inputmethod.latin.SuggestedWords;
 import com.android.inputmethod.latin.define.ProductionFlag;
 import com.android.inputmethod.latin.utils.InputTypeUtils;
+import com.android.inputmethod.latin.utils.TextRange;
 import com.android.inputmethod.research.MotionEventReader.ReplayData;
 import com.android.inputmethod.research.ui.SplashScreen;
 
@@ -1220,7 +1220,7 @@
             final RichInputConnection connection) {
         String word = "";
         if (connection != null) {
-            Range range = connection.getWordRangeAtCursor(WHITESPACE_SEPARATORS, 1);
+            TextRange range = connection.getWordRangeAtCursor(WHITESPACE_SEPARATORS, 1);
             if (range != null) {
                 word = range.mWord.toString();
             }
diff --git a/native/jni/Android.mk b/native/jni/Android.mk
index f89eea7..d78da96 100644
--- a/native/jni/Android.mk
+++ b/native/jni/Android.mk
@@ -79,8 +79,9 @@
         typing_traversal.cpp \
         typing_weighting.cpp) \
     $(addprefix utils/, \
+        autocorrection_threshold_utils.cpp \
         char_utils.cpp \
-        autocorrection_threshold_utils.cpp)
+        log_utils.cpp)
 
 LOCAL_SRC_FILES := \
     $(LATIN_IME_JNI_SRC_FILES) \
diff --git a/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp b/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
index a93bbeb..2b8dbbc 100644
--- a/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
+++ b/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
@@ -93,8 +93,8 @@
         AKLOGE("DICT: dictionary format is unknown, bad magic number");
         releaseDictBuf(static_cast<const char *>(dictBuf) - offset, adjDictSize, fd);
     } else {
-        dictionary = new Dictionary(
-                dictBuf, static_cast<int>(dictSize), fd, offset, updatableMmap);
+        dictionary = new Dictionary(env, dictBuf, static_cast<int>(dictSize), fd, offset,
+                updatableMmap);
     }
     PROF_END(66);
     PROF_CLOSE;
diff --git a/native/jni/src/defines.h b/native/jni/src/defines.h
index cb66814..607a744 100644
--- a/native/jni/src/defines.h
+++ b/native/jni/src/defines.h
@@ -35,6 +35,56 @@
 // Must be equal to ProximityInfo.MAX_PROXIMITY_CHARS_SIZE in Java
 #define MAX_PROXIMITY_CHARS_SIZE 16
 #define ADDITIONAL_PROXIMITY_CHAR_DELIMITER_CODE 2
+#define NELEMS(x) (sizeof(x) / sizeof((x)[0]))
+
+AK_FORCE_INLINE static int intArrayToCharArray(const int *const source, const int sourceSize,
+        char *dest, const int destSize) {
+    // We want to always terminate with a 0 char, so stop one short of the length to make
+    // sure there is room.
+    const int destLimit = destSize - 1;
+    int si = 0;
+    int di = 0;
+    while (si < sourceSize && di < destLimit && 0 != source[si]) {
+        const int codePoint = source[si++];
+        if (codePoint < 0x7F) { // One byte
+            dest[di++] = codePoint;
+        } else if (codePoint < 0x7FF) { // Two bytes
+            if (di + 1 >= destLimit) break;
+            dest[di++] = 0xC0 + (codePoint >> 6);
+            dest[di++] = 0x80 + (codePoint & 0x3F);
+        } else if (codePoint < 0xFFFF) { // Three bytes
+            if (di + 2 >= destLimit) break;
+            dest[di++] = 0xE0 + (codePoint >> 12);
+            dest[di++] = 0x80 + ((codePoint >> 6) & 0x3F);
+            dest[di++] = 0x80 + (codePoint & 0x3F);
+        } else if (codePoint <= 0x1FFFFF) { // Four bytes
+            if (di + 3 >= destLimit) break;
+            dest[di++] = 0xF0 + (codePoint >> 18);
+            dest[di++] = 0x80 + ((codePoint >> 12) & 0x3F);
+            dest[di++] = 0x80 + ((codePoint >> 6) & 0x3F);
+            dest[di++] = 0x80 + (codePoint & 0x3F);
+        } else if (codePoint <= 0x3FFFFFF) { // Five bytes
+            if (di + 4 >= destLimit) break;
+            dest[di++] = 0xF8 + (codePoint >> 24);
+            dest[di++] = 0x80 + ((codePoint >> 18) & 0x3F);
+            dest[di++] = 0x80 + ((codePoint >> 12) & 0x3F);
+            dest[di++] = 0x80 + ((codePoint >> 6) & 0x3F);
+            dest[di++] = codePoint & 0x3F;
+        } else if (codePoint <= 0x7FFFFFFF) { // Six bytes
+            if (di + 5 >= destLimit) break;
+            dest[di++] = 0xFC + (codePoint >> 30);
+            dest[di++] = 0x80 + ((codePoint >> 24) & 0x3F);
+            dest[di++] = 0x80 + ((codePoint >> 18) & 0x3F);
+            dest[di++] = 0x80 + ((codePoint >> 12) & 0x3F);
+            dest[di++] = 0x80 + ((codePoint >> 6) & 0x3F);
+            dest[di++] = codePoint & 0x3F;
+        } else {
+            // Not a code point... skip.
+        }
+    }
+    dest[di] = 0;
+    return di;
+}
 
 #if defined(FLAG_DO_PROFILE) || defined(FLAG_DBG)
 #include <android/log.h>
@@ -46,35 +96,13 @@
 
 #define DUMP_RESULT(words, frequencies) do { dumpResult(words, frequencies); } while (0)
 #define DUMP_WORD(word, length) do { dumpWord(word, length); } while (0)
-#define INTS_TO_CHARS(input, length, output) do { \
-        intArrayToCharArray(input, length, output); } while (0)
-
-// TODO: Support full UTF-8 conversion
-AK_FORCE_INLINE static int intArrayToCharArray(const int *source, const int sourceSize,
-        char *dest) {
-    int si = 0;
-    int di = 0;
-    while (si < sourceSize && di < MAX_WORD_LENGTH - 1 && 0 != source[si]) {
-        const int codePoint = source[si++];
-        if (codePoint < 0x7F) {
-            dest[di++] = codePoint;
-        } else if (codePoint < 0x7FF) {
-            dest[di++] = 0xC0 + (codePoint >> 6);
-            dest[di++] = 0x80 + (codePoint & 0x3F);
-        } else if (codePoint < 0xFFFF) {
-            dest[di++] = 0xE0 + (codePoint >> 12);
-            dest[di++] = 0x80 + ((codePoint & 0xFC0) >> 6);
-            dest[di++] = 0x80 + (codePoint & 0x3F);
-        }
-    }
-    dest[di] = 0;
-    return di;
-}
+#define INTS_TO_CHARS(input, length, output, outlength) do { \
+        intArrayToCharArray(input, length, output, outlength); } while (0)
 
 static inline void dumpWordInfo(const int *word, const int length, const int rank,
         const int probability) {
     static char charBuf[50];
-    const int N = intArrayToCharArray(word, length, charBuf);
+    const int N = intArrayToCharArray(word, length, charBuf, NELEMS(charBuf));
     if (N > 1) {
         AKLOGI("%2d [ %s ] (%d)", rank, charBuf, probability);
     }
@@ -90,7 +118,7 @@
 
 static AK_FORCE_INLINE void dumpWord(const int *word, const int length) {
     static char charBuf[50];
-    const int N = intArrayToCharArray(word, length, charBuf);
+    const int N = intArrayToCharArray(word, length, charBuf, NELEMS(charBuf));
     if (N > 1) {
         AKLOGI("[ %s ]", charBuf);
     }
@@ -304,8 +332,6 @@
 template<typename T> AK_FORCE_INLINE const T &min(const T &a, const T &b) { return a < b ? a : b; }
 template<typename T> AK_FORCE_INLINE const T &max(const T &a, const T &b) { return a > b ? a : b; }
 
-#define NELEMS(x) (sizeof(x) / sizeof((x)[0]))
-
 // DEBUG
 #define INPUTLENGTH_FOR_DEBUG (-1)
 #define MIN_OUTPUT_INDEX_FOR_DEBUG (-1)
diff --git a/native/jni/src/suggest/core/dicnode/dic_node.h b/native/jni/src/suggest/core/dicnode/dic_node.h
index 52db8e9..017df34 100644
--- a/native/jni/src/suggest/core/dicnode/dic_node.h
+++ b/native/jni/src/suggest/core/dicnode/dic_node.h
@@ -28,15 +28,16 @@
 #if DEBUG_DICT
 #define LOGI_SHOW_ADD_COST_PROP \
         do { char charBuf[50]; \
-        INTS_TO_CHARS(getOutputWordBuf(), getNodeCodePointCount(), charBuf); \
+        INTS_TO_CHARS(getOutputWordBuf(), getNodeCodePointCount(), charBuf, NELEMS(charBuf)); \
         AKLOGI("%20s, \"%c\", size = %03d, total = %03d, index(0) = %02d, dist = %.4f, %s,,", \
                 __FUNCTION__, getNodeCodePoint(), inputSize, getTotalInputIndex(), \
                 getInputIndex(0), getNormalizedCompoundDistance(), charBuf); } while (0)
 #define DUMP_WORD_AND_SCORE(header) \
         do { char charBuf[50]; char prevWordCharBuf[50]; \
-        INTS_TO_CHARS(getOutputWordBuf(), getNodeCodePointCount(), charBuf); \
+        INTS_TO_CHARS(getOutputWordBuf(), getNodeCodePointCount(), charBuf, NELEMS(charBuf)); \
         INTS_TO_CHARS(mDicNodeState.mDicNodeStatePrevWord.mPrevWord, \
-                mDicNodeState.mDicNodeStatePrevWord.getPrevWordLength(), prevWordCharBuf); \
+                mDicNodeState.mDicNodeStatePrevWord.getPrevWordLength(), prevWordCharBuf, \
+                NELEMS(prevWordCharBuf)); \
         AKLOGI("#%8s, %5f, %5f, %5f, %5f, %s, %s, %d,,", header, \
                 getSpatialDistanceForScoring(), getLanguageDistanceForScoring(), \
                 getNormalizedCompoundDistance(), getRawLength(), prevWordCharBuf, charBuf, \
diff --git a/native/jni/src/suggest/core/dictionary/binary_dictionary_format_utils.cpp b/native/jni/src/suggest/core/dictionary/binary_dictionary_format_utils.cpp
index bbb4ca3..f48386b 100644
--- a/native/jni/src/suggest/core/dictionary/binary_dictionary_format_utils.cpp
+++ b/native/jni/src/suggest/core/dictionary/binary_dictionary_format_utils.cpp
@@ -27,10 +27,6 @@
 /**
  * Format versions
  */
-// Originally, format version 1 had a 16-bit magic number, then the version number `01'
-// then options that must be 0. Hence the first 32-bits of the format are always as follow
-// and it's okay to consider them a magic number as a whole.
-const uint32_t BinaryDictionaryFormatUtils::FORMAT_VERSION_1_MAGIC_NUMBER = 0x78B10100;
 
 // The versions of Latin IME that only handle format version 1 only test for the magic
 // number, so we had to change it so that version 2 files would be rejected by older
@@ -50,12 +46,6 @@
     }
     const uint32_t magicNumber = ByteArrayUtils::readUint32(dict, 0);
     switch (magicNumber) {
-    case FORMAT_VERSION_1_MAGIC_NUMBER:
-        // Format 1 header is exactly 5 bytes long and looks like:
-        // Magic number (2 bytes) 0x78 0xB1
-        // Version number (1 byte) 0x01
-        // Options (2 bytes) must be 0x00 0x00
-        return VERSION_1;
     case FORMAT_VERSION_2_MAGIC_NUMBER:
         // Version 2 dictionaries are at least 12 bytes long.
         // If this dictionary has the version 2 magic number but is less than 12 bytes long,
diff --git a/native/jni/src/suggest/core/dictionary/binary_dictionary_format_utils.h b/native/jni/src/suggest/core/dictionary/binary_dictionary_format_utils.h
index 33618b9..80067b2 100644
--- a/native/jni/src/suggest/core/dictionary/binary_dictionary_format_utils.h
+++ b/native/jni/src/suggest/core/dictionary/binary_dictionary_format_utils.h
@@ -33,10 +33,9 @@
  */
 class BinaryDictionaryFormatUtils {
  public:
-    // TODO: Remove obsolete version logic
+    // TODO: Support version 3 format.
     enum FORMAT_VERSION {
-        VERSION_1,
-        VERSION_2,
+        VERSION_2 = 1,
         UNKNOWN_VERSION
     };
 
@@ -46,7 +45,6 @@
     DISALLOW_IMPLICIT_CONSTRUCTORS(BinaryDictionaryFormatUtils);
 
     static const int DICTIONARY_MINIMUM_SIZE;
-    static const uint32_t FORMAT_VERSION_1_MAGIC_NUMBER;
     static const uint32_t FORMAT_VERSION_2_MAGIC_NUMBER;
     static const int FORMAT_VERSION_2_MINIMUM_SIZE;
 };
diff --git a/native/jni/src/suggest/core/dictionary/binary_dictionary_header.h b/native/jni/src/suggest/core/dictionary/binary_dictionary_header.h
index 6dba0b2..240512b 100644
--- a/native/jni/src/suggest/core/dictionary/binary_dictionary_header.h
+++ b/native/jni/src/suggest/core/dictionary/binary_dictionary_header.h
@@ -53,6 +53,20 @@
         return mMultiWordCostMultiplier;
     }
 
+    AK_FORCE_INLINE void readHeaderValueOrQuestionMark(const char *const key,
+            int *outValue, int outValueSize) const {
+        if (outValueSize <= 0) return;
+        if (outValueSize == 1) {
+            outValue[0] = '\0';
+            return;
+        }
+        if (!BinaryDictionaryHeaderReadingUtils::readHeaderValue(mBinaryDictionaryInfo,
+                key, outValue, outValueSize)) {
+            outValue[0] = '?';
+            outValue[1] = '\0';
+        }
+    }
+
  private:
     DISALLOW_COPY_AND_ASSIGN(BinaryDictionaryHeader);
 
diff --git a/native/jni/src/suggest/core/dictionary/binary_dictionary_header_reading_utils.cpp b/native/jni/src/suggest/core/dictionary/binary_dictionary_header_reading_utils.cpp
index 2c95931..c4c4bed 100644
--- a/native/jni/src/suggest/core/dictionary/binary_dictionary_header_reading_utils.cpp
+++ b/native/jni/src/suggest/core/dictionary/binary_dictionary_header_reading_utils.cpp
@@ -26,8 +26,6 @@
 
 const int BinaryDictionaryHeaderReadingUtils::MAX_OPTION_KEY_LENGTH = 256;
 
-const int BinaryDictionaryHeaderReadingUtils::FORMAT_VERSION_1_HEADER_SIZE = 5;
-
 const int BinaryDictionaryHeaderReadingUtils::VERSION_2_MAGIC_NUMBER_SIZE = 4;
 const int BinaryDictionaryHeaderReadingUtils::VERSION_2_DICTIONARY_VERSION_SIZE = 2;
 const int BinaryDictionaryHeaderReadingUtils::VERSION_2_DICTIONARY_FLAG_SIZE = 2;
@@ -48,8 +46,6 @@
 /* static */ int BinaryDictionaryHeaderReadingUtils::getHeaderSize(
         const BinaryDictionaryInfo *const binaryDictionaryInfo) {
     switch (binaryDictionaryInfo->getFormat()) {
-        case BinaryDictionaryFormatUtils::VERSION_1:
-            return FORMAT_VERSION_1_HEADER_SIZE;
         case BinaryDictionaryFormatUtils::VERSION_2:
             // See the format of the header in the comment in
             // BinaryDictionaryFormatUtils::detectFormatVersion()
@@ -65,8 +61,6 @@
         BinaryDictionaryHeaderReadingUtils::getFlags(
                 const BinaryDictionaryInfo *const binaryDictionaryInfo) {
     switch (binaryDictionaryInfo->getFormat()) {
-        case BinaryDictionaryFormatUtils::VERSION_1:
-            return NO_FLAGS;
         case BinaryDictionaryFormatUtils::VERSION_2:
             return ByteArrayUtils::readUint16(binaryDictionaryInfo->getDictBuf(),
                     VERSION_2_MAGIC_NUMBER_SIZE + VERSION_2_DICTIONARY_VERSION_SIZE);
@@ -88,8 +82,10 @@
         if(ByteArrayUtils::compareStringInBufferWithCharArray(
                 binaryDictionaryInfo->getDictBuf(), key, headerSize - pos, &pos) == 0) {
             // The key was found.
-            ByteArrayUtils::readStringAndAdvancePosition(
+            const int length = ByteArrayUtils::readStringAndAdvancePosition(
                     binaryDictionaryInfo->getDictBuf(), outValueSize, outValue, &pos);
+            // Add a 0 terminator to the string.
+            outValue[length < outValueSize ? length : outValueSize - 1] = '\0';
             return true;
         }
         ByteArrayUtils::advancePositionToBehindString(
diff --git a/native/jni/src/suggest/core/dictionary/binary_dictionary_header_reading_utils.h b/native/jni/src/suggest/core/dictionary/binary_dictionary_header_reading_utils.h
index 49ed2b9..94b9e12 100644
--- a/native/jni/src/suggest/core/dictionary/binary_dictionary_header_reading_utils.h
+++ b/native/jni/src/suggest/core/dictionary/binary_dictionary_header_reading_utils.h
@@ -82,8 +82,6 @@
  private:
     DISALLOW_IMPLICIT_CONSTRUCTORS(BinaryDictionaryHeaderReadingUtils);
 
-    static const int FORMAT_VERSION_1_HEADER_SIZE;
-
     static const int VERSION_2_MAGIC_NUMBER_SIZE;
     static const int VERSION_2_DICTIONARY_VERSION_SIZE;
     static const int VERSION_2_DICTIONARY_FLAG_SIZE;
diff --git a/native/jni/src/suggest/core/dictionary/binary_dictionary_info.h b/native/jni/src/suggest/core/dictionary/binary_dictionary_info.h
index 7cb3144..cbea18f 100644
--- a/native/jni/src/suggest/core/dictionary/binary_dictionary_info.h
+++ b/native/jni/src/suggest/core/dictionary/binary_dictionary_info.h
@@ -20,23 +20,27 @@
 #include <stdint.h>
 
 #include "defines.h"
+#include "jni.h"
 #include "suggest/core/dictionary/binary_dictionary_format_utils.h"
 #include "suggest/core/dictionary/binary_dictionary_header.h"
 #include "suggest/policyimpl/dictionary/dictionary_structure_policy_factory.h"
+#include "utils/log_utils.h"
 
 namespace latinime {
 
 class BinaryDictionaryInfo {
  public:
-    BinaryDictionaryInfo(const uint8_t *const dictBuf, const int dictSize, const int mmapFd,
-            const int dictBufOffset, const bool isUpdatable)
+     AK_FORCE_INLINE BinaryDictionaryInfo(JNIEnv *env, const uint8_t *const dictBuf,
+            const int dictSize, const int mmapFd, const int dictBufOffset, const bool isUpdatable)
             : mDictBuf(dictBuf), mDictSize(dictSize), mMmapFd(mmapFd),
               mDictBufOffset(dictBufOffset), mIsUpdatable(isUpdatable),
               mDictionaryFormat(BinaryDictionaryFormatUtils::detectFormatVersion(
                       mDictBuf, mDictSize)),
               mDictionaryHeader(this), mDictRoot(mDictBuf + mDictionaryHeader.getSize()),
               mStructurePolicy(DictionaryStructurePolicyFactory::getDictionaryStructurePolicy(
-                      mDictionaryFormat)) {}
+                      mDictionaryFormat)) {
+        logDictionaryInfo(env);
+    }
 
     AK_FORCE_INLINE const uint8_t *getDictBuf() const {
         return mDictBuf;
@@ -88,6 +92,33 @@
     const BinaryDictionaryHeader mDictionaryHeader;
     const uint8_t *const mDictRoot;
     const DictionaryStructurePolicy *const mStructurePolicy;
+
+    AK_FORCE_INLINE void logDictionaryInfo(JNIEnv *const env) const {
+        const int BUFFER_SIZE = 16;
+        int dictionaryIdCodePointBuffer[BUFFER_SIZE];
+        int versionStringCodePointBuffer[BUFFER_SIZE];
+        int dateStringCodePointBuffer[BUFFER_SIZE];
+        mDictionaryHeader.readHeaderValueOrQuestionMark("dictionary",
+                dictionaryIdCodePointBuffer, BUFFER_SIZE);
+        mDictionaryHeader.readHeaderValueOrQuestionMark("version",
+                versionStringCodePointBuffer, BUFFER_SIZE);
+        mDictionaryHeader.readHeaderValueOrQuestionMark("date",
+                dateStringCodePointBuffer, BUFFER_SIZE);
+
+        char dictionaryIdCharBuffer[BUFFER_SIZE];
+        char versionStringCharBuffer[BUFFER_SIZE];
+        char dateStringCharBuffer[BUFFER_SIZE];
+        intArrayToCharArray(dictionaryIdCodePointBuffer, BUFFER_SIZE,
+                dictionaryIdCharBuffer, BUFFER_SIZE);
+        intArrayToCharArray(versionStringCodePointBuffer, BUFFER_SIZE,
+                versionStringCharBuffer, BUFFER_SIZE);
+        intArrayToCharArray(dateStringCodePointBuffer, BUFFER_SIZE,
+                dateStringCharBuffer, BUFFER_SIZE);
+
+        LogUtils::logToJava(env,
+                "Dictionary info: dictionary = %s ; version = %s ; date = %s ; filesize = %i",
+                dictionaryIdCharBuffer, versionStringCharBuffer, dateStringCharBuffer, mDictSize);
+    }
 };
 }
 #endif /* LATINIME_BINARY_DICTIONARY_INFO_H */
diff --git a/native/jni/src/suggest/core/dictionary/dictionary.cpp b/native/jni/src/suggest/core/dictionary/dictionary.cpp
index 675b549..f520a75 100644
--- a/native/jni/src/suggest/core/dictionary/dictionary.cpp
+++ b/native/jni/src/suggest/core/dictionary/dictionary.cpp
@@ -22,6 +22,7 @@
 #include <stdint.h>
 
 #include "defines.h"
+#include "jni.h"
 #include "suggest/core/dictionary/bigram_dictionary.h"
 #include "suggest/core/dictionary/binary_format.h"
 #include "suggest/core/session/dic_traverse_session.h"
@@ -32,8 +33,9 @@
 
 namespace latinime {
 
-Dictionary::Dictionary(void *dict, int dictSize, int mmapFd, int dictBufOffset, bool isUpdatable)
-        : mBinaryDictionaryInfo(static_cast<const uint8_t *>(dict), dictSize, mmapFd,
+Dictionary::Dictionary(JNIEnv *env, void *dict, int dictSize, int mmapFd,
+        int dictBufOffset, bool isUpdatable)
+        : mBinaryDictionaryInfo(env, static_cast<const uint8_t *>(dict), dictSize, mmapFd,
                 dictBufOffset, isUpdatable),
           mBigramDictionary(new BigramDictionary(&mBinaryDictionaryInfo)),
           mGestureSuggest(new Suggest(GestureSuggestPolicyFactory::getGestureSuggestPolicy())),
diff --git a/native/jni/src/suggest/core/dictionary/dictionary.h b/native/jni/src/suggest/core/dictionary/dictionary.h
index 94579c2..1bf24a8 100644
--- a/native/jni/src/suggest/core/dictionary/dictionary.h
+++ b/native/jni/src/suggest/core/dictionary/dictionary.h
@@ -20,6 +20,7 @@
 #include <stdint.h>
 
 #include "defines.h"
+#include "jni.h"
 #include "suggest/core/dictionary/binary_dictionary_info.h"
 
 namespace latinime {
@@ -52,7 +53,8 @@
     static const int KIND_FLAG_POSSIBLY_OFFENSIVE = 0x80000000;
     static const int KIND_FLAG_EXACT_MATCH = 0x40000000;
 
-    Dictionary(void *dict, int dictSize, int mmapFd, int dictBufOffset, bool isUpdatable);
+    Dictionary(JNIEnv *env, void *dict, int dictSize, int mmapFd, int dictBufOffset,
+            bool isUpdatable);
 
     int getSuggestions(ProximityInfo *proximityInfo, DicTraverseSession *traverseSession,
             int *xcoordinates, int *ycoordinates, int *times, int *pointerIds, int *inputCodePoints,
diff --git a/native/jni/src/suggest/policyimpl/dictionary/dictionary_structure_policy_factory.h b/native/jni/src/suggest/policyimpl/dictionary/dictionary_structure_policy_factory.h
index 5070651..c0e24fa 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/dictionary_structure_policy_factory.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/dictionary_structure_policy_factory.h
@@ -30,8 +30,6 @@
     static const DictionaryStructurePolicy *getDictionaryStructurePolicy(
             const BinaryDictionaryFormatUtils::FORMAT_VERSION dictionaryFormat) {
         switch (dictionaryFormat) {
-            case BinaryDictionaryFormatUtils::VERSION_1:
-                // Fall through
             case BinaryDictionaryFormatUtils::VERSION_2:
                 return PatriciaTriePolicy::getInstance();
             default:
diff --git a/native/jni/src/utils/log_utils.cpp b/native/jni/src/utils/log_utils.cpp
new file mode 100644
index 0000000..5ab2b28
--- /dev/null
+++ b/native/jni/src/utils/log_utils.cpp
@@ -0,0 +1,72 @@
+/*
+ * 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.
+ */
+
+#include "log_utils.h"
+
+#include <cstdio>
+#include <stdarg.h>
+
+#include "defines.h"
+
+namespace latinime {
+    /* static */ void LogUtils::logToJava(JNIEnv *const env, const char *const format, ...) {
+        static const char *TAG = "LatinIME:LogUtils";
+        const jclass androidUtilLogClass = env->FindClass("android/util/Log");
+        if (!androidUtilLogClass) {
+            // If we can't find the class, we are probably in off-device testing, and
+            // it's expected. Regardless, logging is not essential to functionality, so
+            // we should just return. However, FindClass has thrown an exception behind
+            // our back and there is no way to prevent it from doing that, so we clear
+            // the exception before we return.
+            env->ExceptionClear();
+            return;
+        }
+        const jmethodID logDotIMethodId = env->GetStaticMethodID(androidUtilLogClass, "i",
+                "(Ljava/lang/String;Ljava/lang/String;)I");
+        if (!logDotIMethodId) {
+            env->ExceptionClear();
+            if (androidUtilLogClass) env->DeleteLocalRef(androidUtilLogClass);
+            return;
+        }
+        const jstring javaTag = env->NewStringUTF(TAG);
+
+        static const int DEFAULT_LINE_SIZE = 128;
+        char fixedSizeCString[DEFAULT_LINE_SIZE];
+        va_list argList;
+        va_start(argList, format);
+        // Get the necessary size. Add 1 for the 0 terminator.
+        const int size = vsnprintf(fixedSizeCString, DEFAULT_LINE_SIZE, format, argList) + 1;
+        va_end(argList);
+
+        jstring javaString;
+        if (size <= DEFAULT_LINE_SIZE) {
+            // The buffer was large enough.
+            javaString = env->NewStringUTF(fixedSizeCString);
+        } else {
+            // The buffer was not large enough.
+            va_start(argList, format);
+            char variableSizeCString[size];
+            vsnprintf(variableSizeCString, size, format, argList);
+            va_end(argList);
+            javaString = env->NewStringUTF(variableSizeCString);
+        }
+
+        env->CallStaticIntMethod(androidUtilLogClass, logDotIMethodId, javaTag, javaString);
+        if (javaString) env->DeleteLocalRef(javaString);
+        if (javaTag) env->DeleteLocalRef(javaTag);
+        if (androidUtilLogClass) env->DeleteLocalRef(androidUtilLogClass);
+    }
+}
diff --git a/native/jni/src/utils/log_utils.h b/native/jni/src/utils/log_utils.h
new file mode 100644
index 0000000..6ac16d9
--- /dev/null
+++ b/native/jni/src/utils/log_utils.h
@@ -0,0 +1,37 @@
+/*
+ * 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.
+ */
+
+#ifndef LATINIME_LOG_UTILS_H
+#define LATINIME_LOG_UTILS_H
+
+#include "defines.h"
+#include "jni.h"
+
+namespace latinime {
+
+class LogUtils {
+ public:
+    static void logToJava(JNIEnv *const env, const char *const format, ...)
+#ifdef __GNUC__
+        __attribute__ ((format (printf, 2, 3)))
+#endif // __GNUC__
+        ;
+
+ private:
+    DISALLOW_COPY_AND_ASSIGN(LogUtils);
+};
+} // namespace latinime
+#endif // LATINIME_LOG_UTILS_H
diff --git a/tests/src/com/android/inputmethod/latin/RichInputConnectionAndTextRangeTests.java b/tests/src/com/android/inputmethod/latin/RichInputConnectionAndTextRangeTests.java
index 0e077bb..c0dd993 100644
--- a/tests/src/com/android/inputmethod/latin/RichInputConnectionAndTextRangeTests.java
+++ b/tests/src/com/android/inputmethod/latin/RichInputConnectionAndTextRangeTests.java
@@ -16,6 +16,8 @@
 
 package com.android.inputmethod.latin;
 
+import com.android.inputmethod.latin.utils.TextRange;
+
 import android.inputmethodservice.InputMethodService;
 import android.os.Parcel;
 import android.test.AndroidTestCase;
@@ -30,8 +32,6 @@
 import android.view.inputmethod.InputConnection;
 import android.view.inputmethod.InputConnectionWrapper;
 
-import com.android.inputmethod.latin.RichInputConnection.Range;
-
 import java.util.Locale;
 
 @SmallTest
@@ -169,7 +169,7 @@
         mockInputMethodService.setInputConnection(new MockConnection("word wo", "rd", et));
         et.startOffset = 0;
         et.selectionStart = 7;
-        Range r;
+        TextRange r;
 
         ic.beginBatchEdit();
         // basic case
@@ -241,7 +241,7 @@
         text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS1, 0 /* flags */),
                 10 /* start */, 16 /* end */, 0 /* flags */);
         mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos));
-        Range r;
+        TextRange r;
         SuggestionSpan[] suggestions;
 
         r = ic.getWordRangeAtCursor(" ", 0);
diff --git a/tests/src/com/android/inputmethod/latin/WordComposerTests.java b/tests/src/com/android/inputmethod/latin/WordComposerTests.java
new file mode 100644
index 0000000..1434c6b
--- /dev/null
+++ b/tests/src/com/android/inputmethod/latin/WordComposerTests.java
@@ -0,0 +1,93 @@
+/*
+ * 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;
+
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+/**
+ * Unit tests for WordComposer.
+ */
+@SmallTest
+public class WordComposerTests extends AndroidTestCase {
+    public void testMoveCursor() {
+        final WordComposer wc = new WordComposer();
+        final String STR_WITHIN_BMP = "abcdef";
+        wc.setComposingWord(STR_WITHIN_BMP, null);
+        assertEquals(wc.size(),
+                STR_WITHIN_BMP.codePointCount(0, STR_WITHIN_BMP.length()));
+        assertFalse(wc.isCursorFrontOrMiddleOfComposingWord());
+        wc.setCursorPositionWithinWord(2);
+        assertTrue(wc.isCursorFrontOrMiddleOfComposingWord());
+        // Move the cursor to after the 'd'
+        assertTrue(wc.moveCursorByAndReturnIfInsideComposingWord(2));
+        assertTrue(wc.isCursorFrontOrMiddleOfComposingWord());
+        // Move the cursor to after the 'e'
+        assertTrue(wc.moveCursorByAndReturnIfInsideComposingWord(1));
+        assertTrue(wc.isCursorFrontOrMiddleOfComposingWord());
+        assertEquals(wc.size(), 6);
+        // Move the cursor to after the 'f'
+        assertTrue(wc.moveCursorByAndReturnIfInsideComposingWord(1));
+        assertFalse(wc.isCursorFrontOrMiddleOfComposingWord());
+        // Move the cursor past the end of the word
+        assertFalse(wc.moveCursorByAndReturnIfInsideComposingWord(1));
+        assertFalse(wc.moveCursorByAndReturnIfInsideComposingWord(15));
+
+        // \uD861\uDED7 is 𨛗, a character outside the BMP
+        final String STR_WITH_SUPPLEMENTARY_CHAR = "abcde\uD861\uDED7fgh";
+        wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, null);
+        assertEquals(wc.size(), STR_WITH_SUPPLEMENTARY_CHAR.codePointCount(0,
+                        STR_WITH_SUPPLEMENTARY_CHAR.length()));
+        assertFalse(wc.isCursorFrontOrMiddleOfComposingWord());
+        wc.setCursorPositionWithinWord(3);
+        assertTrue(wc.isCursorFrontOrMiddleOfComposingWord());
+        assertTrue(wc.moveCursorByAndReturnIfInsideComposingWord(6));
+        assertTrue(wc.isCursorFrontOrMiddleOfComposingWord());
+        assertTrue(wc.moveCursorByAndReturnIfInsideComposingWord(1));
+        assertFalse(wc.isCursorFrontOrMiddleOfComposingWord());
+
+        wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, null);
+        wc.setCursorPositionWithinWord(3);
+        assertTrue(wc.moveCursorByAndReturnIfInsideComposingWord(7));
+
+        wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, null);
+        wc.setCursorPositionWithinWord(3);
+        assertTrue(wc.moveCursorByAndReturnIfInsideComposingWord(7));
+
+        wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, null);
+        wc.setCursorPositionWithinWord(3);
+        assertTrue(wc.moveCursorByAndReturnIfInsideComposingWord(-3));
+        assertFalse(wc.moveCursorByAndReturnIfInsideComposingWord(-1));
+
+        wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, null);
+        wc.setCursorPositionWithinWord(3);
+        assertFalse(wc.moveCursorByAndReturnIfInsideComposingWord(-9));
+
+        wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, null);
+        assertTrue(wc.moveCursorByAndReturnIfInsideComposingWord(-10));
+
+        wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, null);
+        assertFalse(wc.moveCursorByAndReturnIfInsideComposingWord(-11));
+
+        wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, null);
+        assertTrue(wc.moveCursorByAndReturnIfInsideComposingWord(0));
+
+        wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, null);
+        wc.setCursorPositionWithinWord(2);
+        assertTrue(wc.moveCursorByAndReturnIfInsideComposingWord(0));
+    }
+}
diff --git a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtilsTests.java b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtilsTests.java
index d33142c..9331da4 100644
--- a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtilsTests.java
+++ b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtilsTests.java
@@ -44,9 +44,10 @@
     private static final String TAG = BinaryDictIOUtilsTests.class.getSimpleName();
     private static final FormatSpec.FormatOptions FORMAT_OPTIONS =
             new FormatSpec.FormatOptions(3, true);
-    private static final int MAX_UNIGRAMS = 1500;
 
     private static final ArrayList<String> sWords = CollectionUtils.newArrayList();
+    public static final int DEFAULT_MAX_UNIGRAMS = 1500;
+    private final int mMaxUnigrams;
 
     private static final String[] CHARACTERS = {
         "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m",
@@ -57,15 +58,17 @@
     };
 
     public BinaryDictIOUtilsTests() {
-        this(System.currentTimeMillis());
+        // 1500 is the default max unigrams
+        this(System.currentTimeMillis(), DEFAULT_MAX_UNIGRAMS);
     }
 
-    public BinaryDictIOUtilsTests(final long seed) {
+    public BinaryDictIOUtilsTests(final long seed, final int maxUnigrams) {
         super();
-        Log.d(TAG, "Seed for test is " + seed);
+        Log.d(TAG, "Seed for test is " + seed + ", maxUnigrams is " + maxUnigrams);
+        mMaxUnigrams = maxUnigrams;
         final Random random = new Random(seed);
         sWords.clear();
-        for (int i = 0; i < MAX_UNIGRAMS; ++i) {
+        for (int i = 0; i < maxUnigrams; ++i) {
             sWords.add(generateWord(random.nextInt()));
         }
     }
@@ -395,6 +398,6 @@
 
         Log.d(TAG, "max = " + ((double)maxTimeToInsert/1000000) + " ms.");
         Log.d(TAG, "min = " + ((double)minTimeToInsert/1000000) + " ms.");
-        Log.d(TAG, "avg = " + ((double)sum/MAX_UNIGRAMS/1000000) + " ms.");
+        Log.d(TAG, "avg = " + ((double)sum/mMaxUnigrams/1000000) + " ms.");
     }
 }
diff --git a/tools/dicttool/src/com/android/inputmethod/latin/dicttool/Test.java b/tools/dicttool/src/com/android/inputmethod/latin/dicttool/Test.java
index 827c5e3..df5ea35 100644
--- a/tools/dicttool/src/com/android/inputmethod/latin/dicttool/Test.java
+++ b/tools/dicttool/src/com/android/inputmethod/latin/dicttool/Test.java
@@ -19,33 +19,80 @@
 import com.android.inputmethod.latin.makedict.BinaryDictIOUtilsTests;
 import com.android.inputmethod.latin.makedict.BinaryDictInputOutputTest;
 import com.android.inputmethod.latin.makedict.FusionDictionaryTest;
-import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
 
-import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
 
 /**
  * Dicttool command implementing self-tests.
  */
 public class Test extends Dicttool.Command {
     public static final String COMMAND = "test";
+    private long mSeed = System.currentTimeMillis();
+    private int mMaxUnigrams = BinaryDictIOUtilsTests.DEFAULT_MAX_UNIGRAMS;
+
+    private static final Class<?>[] sClassesToTest = {
+        BinaryDictOffdeviceUtilsTests.class,
+        FusionDictionaryTest.class,
+        BinaryDictInputOutputTest.class,
+        BinaryDictIOUtilsTests.class
+    };
+    private ArrayList<Method> mAllTestMethods = new ArrayList<Method>();
+    private ArrayList<String> mUsedTestMethods = new ArrayList<String>();
 
     public Test() {
+        for (final Class<?> c : sClassesToTest) {
+            for (final Method m : c.getDeclaredMethods()) {
+                if (m.getName().startsWith("test") && Void.TYPE == m.getReturnType()
+                        && 0 == m.getParameterTypes().length) {
+                    mAllTestMethods.add(m);
+                }
+            }
+        }
     }
 
     @Override
     public String getHelp() {
-        return "test";
+        final StringBuilder s = new StringBuilder("test [-s seed] [-m maxUnigrams] [testName...]\n"
+                + "If seed is not specified, the current time is used.\nTest list is:\n");
+        for (final Method m : mAllTestMethods) {
+            s.append("  ");
+            s.append(m.getName());
+            s.append("\n");
+        }
+        return s.toString();
     }
 
     @Override
-    public void run() throws IOException, UnsupportedFormatException {
-        test();
+    public void run() throws IllegalAccessException, InstantiationException,
+            InvocationTargetException {
+        int i = 0;
+        while (i < mArgs.length) {
+            final String arg = mArgs[i++];
+            if ("-s".equals(arg)) {
+                mSeed = Long.parseLong(mArgs[i++]);
+            } else if ("-m".equals(arg)) {
+                mMaxUnigrams = Integer.parseInt(mArgs[i++]);
+            } else {
+                mUsedTestMethods.add(arg);
+            }
+        }
+        runChosenTests();
     }
 
-    private void test() throws IOException, UnsupportedFormatException {
-        new BinaryDictOffdeviceUtilsTests().testGetRawDictWorks();
-        new FusionDictionaryTest().testFusion();
-        new BinaryDictInputOutputTest().testFlattenNodes();
-        new BinaryDictIOUtilsTests().testRandomWords();
+    private void runChosenTests() throws IllegalAccessException, InstantiationException,
+            InvocationTargetException {
+        for (final Method m : mAllTestMethods) {
+            final Class<?> declaringClass = m.getDeclaringClass();
+            if (!mUsedTestMethods.isEmpty() && !mUsedTestMethods.contains(m.getName())) continue;
+            final Object instance;
+            if (BinaryDictIOUtilsTests.class == declaringClass) {
+                instance = new BinaryDictIOUtilsTests(mSeed, mMaxUnigrams);
+            } else {
+                instance = declaringClass.newInstance();
+            }
+            m.invoke(instance);
+        }
     }
 }