Merge "Add args to dicttool test."
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/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_reading_utils.cpp b/native/jni/src/suggest/core/dictionary/binary_dictionary_header_reading_utils.cpp
index 2c95931..6e1b15c 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);
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/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/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));
+    }
+}