Merge "Add timestamp as an argument to updating methods."
diff --git a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
index 0fe2455..f7c54c7 100644
--- a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
@@ -31,8 +31,6 @@
 import android.graphics.Paint.Align;
 import android.graphics.Typeface;
 import android.graphics.drawable.Drawable;
-import android.os.Message;
-import android.os.SystemClock;
 import android.preference.PreferenceManager;
 import android.util.AttributeSet;
 import android.util.DisplayMetrics;
@@ -41,7 +39,6 @@
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.View;
-import android.view.ViewConfiguration;
 import android.view.ViewGroup;
 import android.view.animation.AccelerateInterpolator;
 import android.view.animation.DecelerateInterpolator;
@@ -57,6 +54,8 @@
 import com.android.inputmethod.keyboard.internal.GestureTrailsPreview;
 import com.android.inputmethod.keyboard.internal.KeyDrawParams;
 import com.android.inputmethod.keyboard.internal.KeyPreviewDrawParams;
+import com.android.inputmethod.keyboard.internal.MainKeyboardViewDrawingHandler;
+import com.android.inputmethod.keyboard.internal.MainKeyboardViewTimerHandler;
 import com.android.inputmethod.keyboard.internal.NonDistinctMultitouchHelper;
 import com.android.inputmethod.keyboard.internal.PreviewPlacerView;
 import com.android.inputmethod.keyboard.internal.SlidingKeyInputPreview;
@@ -68,7 +67,6 @@
 import com.android.inputmethod.latin.settings.DebugSettings;
 import com.android.inputmethod.latin.utils.CollectionUtils;
 import com.android.inputmethod.latin.utils.CoordinateUtils;
-import com.android.inputmethod.latin.utils.StaticInnerHandlerWrapper;
 import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
 import com.android.inputmethod.latin.utils.TypefaceUtils;
 import com.android.inputmethod.latin.utils.UsabilityStudyLogUtils;
@@ -150,8 +148,8 @@
     private static final int SPACE_LED_LENGTH_PERCENT = 80;
 
     // Stuff to draw altCodeWhileTyping keys.
-    private ObjectAnimator mAltCodeKeyWhileTypingFadeoutAnimator;
-    private ObjectAnimator mAltCodeKeyWhileTypingFadeinAnimator;
+    private final ObjectAnimator mAltCodeKeyWhileTypingFadeoutAnimator;
+    private final ObjectAnimator mAltCodeKeyWhileTypingFadeinAnimator;
     private int mAltCodeKeyWhileTypingAnimAlpha = Constants.Color.ALPHA_OPAQUE;
 
     // Preview placer view
@@ -201,235 +199,11 @@
     private KeyDetector mKeyDetector;
     private final NonDistinctMultitouchHelper mNonDistinctMultitouchHelper;
 
-    private final KeyTimerHandler mKeyTimerHandler;
+    private final MainKeyboardViewTimerHandler mKeyTimerHandler;
     private final int mLanguageOnSpacebarHorizontalMargin;
 
-    private static final class KeyTimerHandler extends StaticInnerHandlerWrapper<MainKeyboardView>
-            implements TimerProxy {
-        private static final int MSG_TYPING_STATE_EXPIRED = 0;
-        private static final int MSG_REPEAT_KEY = 1;
-        private static final int MSG_LONGPRESS_KEY = 2;
-        private static final int MSG_DOUBLE_TAP_SHIFT_KEY = 3;
-        private static final int MSG_UPDATE_BATCH_INPUT = 4;
-
-        private final int mIgnoreAltCodeKeyTimeout;
-        private final int mGestureRecognitionUpdateTime;
-
-        public KeyTimerHandler(final MainKeyboardView outerInstance,
-                final TypedArray mainKeyboardViewAttr) {
-            super(outerInstance);
-
-            mIgnoreAltCodeKeyTimeout = mainKeyboardViewAttr.getInt(
-                    R.styleable.MainKeyboardView_ignoreAltCodeKeyTimeout, 0);
-            mGestureRecognitionUpdateTime = mainKeyboardViewAttr.getInt(
-                    R.styleable.MainKeyboardView_gestureRecognitionUpdateTime, 0);
-        }
-
-        @Override
-        public void handleMessage(final Message msg) {
-            final MainKeyboardView keyboardView = getOuterInstance();
-            if (keyboardView == null) {
-                return;
-            }
-            final PointerTracker tracker = (PointerTracker) msg.obj;
-            switch (msg.what) {
-            case MSG_TYPING_STATE_EXPIRED:
-                startWhileTypingFadeinAnimation(keyboardView);
-                break;
-            case MSG_REPEAT_KEY:
-                tracker.onKeyRepeat(msg.arg1 /* code */, msg.arg2 /* repeatCount */);
-                break;
-            case MSG_LONGPRESS_KEY:
-                keyboardView.onLongPress(tracker);
-                break;
-            case MSG_UPDATE_BATCH_INPUT:
-                tracker.updateBatchInputByTimer(SystemClock.uptimeMillis());
-                startUpdateBatchInputTimer(tracker);
-                break;
-            }
-        }
-
-        @Override
-        public void startKeyRepeatTimer(final PointerTracker tracker, final int repeatCount,
-                final int delay) {
-            final Key key = tracker.getKey();
-            if (key == null || delay == 0) {
-                return;
-            }
-            sendMessageDelayed(
-                    obtainMessage(MSG_REPEAT_KEY, key.getCode(), repeatCount, tracker), delay);
-        }
-
-        public void cancelKeyRepeatTimer() {
-            removeMessages(MSG_REPEAT_KEY);
-        }
-
-        // TODO: Suppress layout changes in key repeat mode
-        public boolean isInKeyRepeat() {
-            return hasMessages(MSG_REPEAT_KEY);
-        }
-
-        @Override
-        public void startLongPressTimer(final PointerTracker tracker, final int delay) {
-            cancelLongPressTimer();
-            if (delay <= 0) return;
-            sendMessageDelayed(obtainMessage(MSG_LONGPRESS_KEY, tracker), delay);
-        }
-
-        @Override
-        public void cancelLongPressTimer() {
-            removeMessages(MSG_LONGPRESS_KEY);
-        }
-
-        private static void cancelAndStartAnimators(final ObjectAnimator animatorToCancel,
-                final ObjectAnimator animatorToStart) {
-            if (animatorToCancel == null || animatorToStart == null) {
-                // TODO: Stop using null as a no-operation animator.
-                return;
-            }
-            float startFraction = 0.0f;
-            if (animatorToCancel.isStarted()) {
-                animatorToCancel.cancel();
-                startFraction = 1.0f - animatorToCancel.getAnimatedFraction();
-            }
-            final long startTime = (long)(animatorToStart.getDuration() * startFraction);
-            animatorToStart.start();
-            animatorToStart.setCurrentPlayTime(startTime);
-        }
-
-        private static void startWhileTypingFadeinAnimation(final MainKeyboardView keyboardView) {
-            cancelAndStartAnimators(keyboardView.mAltCodeKeyWhileTypingFadeoutAnimator,
-                    keyboardView.mAltCodeKeyWhileTypingFadeinAnimator);
-        }
-
-        private static void startWhileTypingFadeoutAnimation(final MainKeyboardView keyboardView) {
-            cancelAndStartAnimators(keyboardView.mAltCodeKeyWhileTypingFadeinAnimator,
-                    keyboardView.mAltCodeKeyWhileTypingFadeoutAnimator);
-        }
-
-        @Override
-        public void startTypingStateTimer(final Key typedKey) {
-            if (typedKey.isModifier() || typedKey.altCodeWhileTyping()) {
-                return;
-            }
-
-            final boolean isTyping = isTypingState();
-            removeMessages(MSG_TYPING_STATE_EXPIRED);
-            final MainKeyboardView keyboardView = getOuterInstance();
-
-            // When user hits the space or the enter key, just cancel the while-typing timer.
-            final int typedCode = typedKey.getCode();
-            if (typedCode == Constants.CODE_SPACE || typedCode == Constants.CODE_ENTER) {
-                if (isTyping) {
-                    startWhileTypingFadeinAnimation(keyboardView);
-                }
-                return;
-            }
-
-            sendMessageDelayed(
-                    obtainMessage(MSG_TYPING_STATE_EXPIRED), mIgnoreAltCodeKeyTimeout);
-            if (isTyping) {
-                return;
-            }
-            startWhileTypingFadeoutAnimation(keyboardView);
-        }
-
-        @Override
-        public boolean isTypingState() {
-            return hasMessages(MSG_TYPING_STATE_EXPIRED);
-        }
-
-        @Override
-        public void startDoubleTapShiftKeyTimer() {
-            sendMessageDelayed(obtainMessage(MSG_DOUBLE_TAP_SHIFT_KEY),
-                    ViewConfiguration.getDoubleTapTimeout());
-        }
-
-        @Override
-        public void cancelDoubleTapShiftKeyTimer() {
-            removeMessages(MSG_DOUBLE_TAP_SHIFT_KEY);
-        }
-
-        @Override
-        public boolean isInDoubleTapShiftKeyTimeout() {
-            return hasMessages(MSG_DOUBLE_TAP_SHIFT_KEY);
-        }
-
-        @Override
-        public void cancelKeyTimers() {
-            cancelKeyRepeatTimer();
-            cancelLongPressTimer();
-        }
-
-        @Override
-        public void startUpdateBatchInputTimer(final PointerTracker tracker) {
-            if (mGestureRecognitionUpdateTime <= 0) {
-                return;
-            }
-            removeMessages(MSG_UPDATE_BATCH_INPUT, tracker);
-            sendMessageDelayed(obtainMessage(MSG_UPDATE_BATCH_INPUT, tracker),
-                    mGestureRecognitionUpdateTime);
-        }
-
-        @Override
-        public void cancelUpdateBatchInputTimer(final PointerTracker tracker) {
-            removeMessages(MSG_UPDATE_BATCH_INPUT, tracker);
-        }
-
-        @Override
-        public void cancelAllUpdateBatchInputTimers() {
-            removeMessages(MSG_UPDATE_BATCH_INPUT);
-        }
-
-        public void cancelAllMessages() {
-            cancelKeyTimers();
-            cancelAllUpdateBatchInputTimers();
-        }
-    }
-
-    private final DrawingHandler mDrawingHandler = new DrawingHandler(this);
-
-    public static class DrawingHandler extends StaticInnerHandlerWrapper<MainKeyboardView> {
-        private static final int MSG_DISMISS_KEY_PREVIEW = 0;
-        private static final int MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT = 1;
-
-        public DrawingHandler(final MainKeyboardView outerInstance) {
-            super(outerInstance);
-        }
-
-        @Override
-        public void handleMessage(final Message msg) {
-            final MainKeyboardView mainKeyboardView = getOuterInstance();
-            if (mainKeyboardView == null) return;
-            switch (msg.what) {
-            case MSG_DISMISS_KEY_PREVIEW:
-                mainKeyboardView.dismissKeyPreviewWithoutDelay((Key)msg.obj);
-                break;
-            case MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT:
-                mainKeyboardView.showGestureFloatingPreviewText(SuggestedWords.EMPTY);
-                break;
-            }
-        }
-
-        public void dismissKeyPreview(final long delay, final Key key) {
-            sendMessageDelayed(obtainMessage(MSG_DISMISS_KEY_PREVIEW, key), delay);
-        }
-
-        private void cancelAllDismissKeyPreviews() {
-            removeMessages(MSG_DISMISS_KEY_PREVIEW);
-            final MainKeyboardView mainKeyboardView = getOuterInstance();
-            if (mainKeyboardView == null) return;
-            mainKeyboardView.dismissAllKeyPreviews();
-        }
-
-        public void dismissGestureFloatingPreviewText(final long delay) {
-            sendMessageDelayed(obtainMessage(MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT), delay);
-        }
-
-        public void cancelAllMessages() {
-            cancelAllDismissKeyPreviews();
-        }
-    }
+    private final MainKeyboardViewDrawingHandler mDrawingHandler =
+            new MainKeyboardViewDrawingHandler(this);
 
     public MainKeyboardView(final Context context, final AttributeSet attrs) {
         this(context, attrs, R.attr.mainKeyboardViewStyle);
@@ -482,7 +256,7 @@
                 R.styleable.MainKeyboardView_keyHysteresisDistanceForSlidingModifier, 0.0f);
         mKeyDetector = new KeyDetector(
                 keyHysteresisDistance, keyHysteresisDistanceForSlidingModifier);
-        mKeyTimerHandler = new KeyTimerHandler(this, mainKeyboardViewAttr);
+        mKeyTimerHandler = new MainKeyboardViewTimerHandler(this, mainKeyboardViewAttr);
         mKeyPreviewOffset = mainKeyboardViewAttr.getDimensionPixelOffset(
                 R.styleable.MainKeyboardView_keyPreviewOffset, 0);
         mKeyPreviewHeight = mainKeyboardViewAttr.getDimensionPixelSize(
@@ -554,6 +328,32 @@
         return animator;
     }
 
+    private static void cancelAndStartAnimators(final ObjectAnimator animatorToCancel,
+            final ObjectAnimator animatorToStart) {
+        if (animatorToCancel == null || animatorToStart == null) {
+            // TODO: Stop using null as a no-operation animator.
+            return;
+        }
+        float startFraction = 0.0f;
+        if (animatorToCancel.isStarted()) {
+            animatorToCancel.cancel();
+            startFraction = 1.0f - animatorToCancel.getAnimatedFraction();
+        }
+        final long startTime = (long)(animatorToStart.getDuration() * startFraction);
+        animatorToStart.start();
+        animatorToStart.setCurrentPlayTime(startTime);
+    }
+
+    public void startWhileTypingFadeinAnimation() {
+        cancelAndStartAnimators(
+                mAltCodeKeyWhileTypingFadeoutAnimator, mAltCodeKeyWhileTypingFadeinAnimator);
+    }
+
+    public void startWhileTypingFadeoutAnimation() {
+        cancelAndStartAnimators(
+                mAltCodeKeyWhileTypingFadeinAnimator, mAltCodeKeyWhileTypingFadeoutAnimator);
+    }
+
     @ExternallyReferenced
     public int getLanguageOnSpacebarAnimAlpha() {
         return mLanguageOnSpacebarAnimAlpha;
@@ -721,7 +521,7 @@
         return previewTextView;
     }
 
-    private void dismissAllKeyPreviews() {
+    public void dismissAllKeyPreviews() {
         for (final Key key : new HashSet<Key>(mShowingKeyPreviewTextViews.keySet())) {
             dismissKeyPreviewWithoutDelay(key);
         }
@@ -915,7 +715,7 @@
     }
 
     // TODO: Take this method out of this class.
-    private void dismissKeyPreviewWithoutDelay(final Key key) {
+    public void dismissKeyPreviewWithoutDelay(final Key key) {
         if (key == null) {
             return;
         }
@@ -1052,7 +852,7 @@
      * Called when a key is long pressed.
      * @param tracker the pointer tracker which pressed the parent key
      */
-    private void onLongPress(final PointerTracker tracker) {
+    public void onLongPress(final PointerTracker tracker) {
         if (isShowingMoreKeysPanel()) {
             return;
         }
diff --git a/java/src/com/android/inputmethod/keyboard/internal/MainKeyboardViewDrawingHandler.java b/java/src/com/android/inputmethod/keyboard/internal/MainKeyboardViewDrawingHandler.java
new file mode 100644
index 0000000..5384553
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/MainKeyboardViewDrawingHandler.java
@@ -0,0 +1,66 @@
+/*
+ * 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.keyboard.internal;
+
+import android.os.Message;
+
+import com.android.inputmethod.keyboard.Key;
+import com.android.inputmethod.keyboard.MainKeyboardView;
+import com.android.inputmethod.latin.SuggestedWords;
+import com.android.inputmethod.latin.utils.StaticInnerHandlerWrapper;
+
+public class MainKeyboardViewDrawingHandler extends StaticInnerHandlerWrapper<MainKeyboardView> {
+    private static final int MSG_DISMISS_KEY_PREVIEW = 0;
+    private static final int MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT = 1;
+
+    public MainKeyboardViewDrawingHandler(final MainKeyboardView outerInstance) {
+        super(outerInstance);
+    }
+
+    @Override
+    public void handleMessage(final Message msg) {
+        final MainKeyboardView mainKeyboardView = getOuterInstance();
+        if (mainKeyboardView == null) return;
+        switch (msg.what) {
+        case MSG_DISMISS_KEY_PREVIEW:
+            mainKeyboardView.dismissKeyPreviewWithoutDelay((Key)msg.obj);
+            break;
+        case MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT:
+            mainKeyboardView.showGestureFloatingPreviewText(SuggestedWords.EMPTY);
+            break;
+        }
+    }
+
+    public void dismissKeyPreview(final long delay, final Key key) {
+        sendMessageDelayed(obtainMessage(MSG_DISMISS_KEY_PREVIEW, key), delay);
+    }
+
+    private void cancelAllDismissKeyPreviews() {
+        removeMessages(MSG_DISMISS_KEY_PREVIEW);
+        final MainKeyboardView mainKeyboardView = getOuterInstance();
+        if (mainKeyboardView == null) return;
+        mainKeyboardView.dismissAllKeyPreviews();
+    }
+
+    public void dismissGestureFloatingPreviewText(final long delay) {
+        sendMessageDelayed(obtainMessage(MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT), delay);
+    }
+
+    public void cancelAllMessages() {
+        cancelAllDismissKeyPreviews();
+    }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/MainKeyboardViewTimerHandler.java b/java/src/com/android/inputmethod/keyboard/internal/MainKeyboardViewTimerHandler.java
new file mode 100644
index 0000000..3ffb2b6
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/MainKeyboardViewTimerHandler.java
@@ -0,0 +1,187 @@
+/*
+ * 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.keyboard.internal;
+
+import android.content.res.TypedArray;
+import android.os.Message;
+import android.os.SystemClock;
+import android.view.ViewConfiguration;
+
+import com.android.inputmethod.keyboard.Key;
+import com.android.inputmethod.keyboard.MainKeyboardView;
+import com.android.inputmethod.keyboard.PointerTracker;
+import com.android.inputmethod.keyboard.PointerTracker.TimerProxy;
+import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.utils.StaticInnerHandlerWrapper;
+
+public final class MainKeyboardViewTimerHandler extends StaticInnerHandlerWrapper<MainKeyboardView>
+        implements TimerProxy {
+    private static final int MSG_TYPING_STATE_EXPIRED = 0;
+    private static final int MSG_REPEAT_KEY = 1;
+    private static final int MSG_LONGPRESS_KEY = 2;
+    private static final int MSG_DOUBLE_TAP_SHIFT_KEY = 3;
+    private static final int MSG_UPDATE_BATCH_INPUT = 4;
+
+    private final int mIgnoreAltCodeKeyTimeout;
+    private final int mGestureRecognitionUpdateTime;
+
+    public MainKeyboardViewTimerHandler(final MainKeyboardView outerInstance,
+            final TypedArray mainKeyboardViewAttr) {
+        super(outerInstance);
+
+        mIgnoreAltCodeKeyTimeout = mainKeyboardViewAttr.getInt(
+                R.styleable.MainKeyboardView_ignoreAltCodeKeyTimeout, 0);
+        mGestureRecognitionUpdateTime = mainKeyboardViewAttr.getInt(
+                R.styleable.MainKeyboardView_gestureRecognitionUpdateTime, 0);
+    }
+
+    @Override
+    public void handleMessage(final Message msg) {
+        final MainKeyboardView keyboardView = getOuterInstance();
+        if (keyboardView == null) {
+            return;
+        }
+        final PointerTracker tracker = (PointerTracker) msg.obj;
+        switch (msg.what) {
+        case MSG_TYPING_STATE_EXPIRED:
+            keyboardView.startWhileTypingFadeinAnimation();
+            break;
+        case MSG_REPEAT_KEY:
+            tracker.onKeyRepeat(msg.arg1 /* code */, msg.arg2 /* repeatCount */);
+            break;
+        case MSG_LONGPRESS_KEY:
+            keyboardView.onLongPress(tracker);
+            break;
+        case MSG_UPDATE_BATCH_INPUT:
+            tracker.updateBatchInputByTimer(SystemClock.uptimeMillis());
+            startUpdateBatchInputTimer(tracker);
+            break;
+        }
+    }
+
+    @Override
+    public void startKeyRepeatTimer(final PointerTracker tracker, final int repeatCount,
+            final int delay) {
+        final Key key = tracker.getKey();
+        if (key == null || delay == 0) {
+            return;
+        }
+        sendMessageDelayed(
+                obtainMessage(MSG_REPEAT_KEY, key.getCode(), repeatCount, tracker), delay);
+    }
+
+    public void cancelKeyRepeatTimer() {
+        removeMessages(MSG_REPEAT_KEY);
+    }
+
+    // TODO: Suppress layout changes in key repeat mode
+    public boolean isInKeyRepeat() {
+        return hasMessages(MSG_REPEAT_KEY);
+    }
+
+    @Override
+    public void startLongPressTimer(final PointerTracker tracker, final int delay) {
+        cancelLongPressTimer();
+        if (delay <= 0) return;
+        sendMessageDelayed(obtainMessage(MSG_LONGPRESS_KEY, tracker), delay);
+    }
+
+    @Override
+    public void cancelLongPressTimer() {
+        removeMessages(MSG_LONGPRESS_KEY);
+    }
+
+    @Override
+    public void startTypingStateTimer(final Key typedKey) {
+        if (typedKey.isModifier() || typedKey.altCodeWhileTyping()) {
+            return;
+        }
+
+        final boolean isTyping = isTypingState();
+        removeMessages(MSG_TYPING_STATE_EXPIRED);
+        final MainKeyboardView keyboardView = getOuterInstance();
+
+        // When user hits the space or the enter key, just cancel the while-typing timer.
+        final int typedCode = typedKey.getCode();
+        if (typedCode == Constants.CODE_SPACE || typedCode == Constants.CODE_ENTER) {
+            if (isTyping) {
+                keyboardView.startWhileTypingFadeinAnimation();
+            }
+            return;
+        }
+
+        sendMessageDelayed(
+                obtainMessage(MSG_TYPING_STATE_EXPIRED), mIgnoreAltCodeKeyTimeout);
+        if (isTyping) {
+            return;
+        }
+        keyboardView.startWhileTypingFadeoutAnimation();
+    }
+
+    @Override
+    public boolean isTypingState() {
+        return hasMessages(MSG_TYPING_STATE_EXPIRED);
+    }
+
+    @Override
+    public void startDoubleTapShiftKeyTimer() {
+        sendMessageDelayed(obtainMessage(MSG_DOUBLE_TAP_SHIFT_KEY),
+                ViewConfiguration.getDoubleTapTimeout());
+    }
+
+    @Override
+    public void cancelDoubleTapShiftKeyTimer() {
+        removeMessages(MSG_DOUBLE_TAP_SHIFT_KEY);
+    }
+
+    @Override
+    public boolean isInDoubleTapShiftKeyTimeout() {
+        return hasMessages(MSG_DOUBLE_TAP_SHIFT_KEY);
+    }
+
+    @Override
+    public void cancelKeyTimers() {
+        cancelKeyRepeatTimer();
+        cancelLongPressTimer();
+    }
+
+    @Override
+    public void startUpdateBatchInputTimer(final PointerTracker tracker) {
+        if (mGestureRecognitionUpdateTime <= 0) {
+            return;
+        }
+        removeMessages(MSG_UPDATE_BATCH_INPUT, tracker);
+        sendMessageDelayed(obtainMessage(MSG_UPDATE_BATCH_INPUT, tracker),
+                mGestureRecognitionUpdateTime);
+    }
+
+    @Override
+    public void cancelUpdateBatchInputTimer(final PointerTracker tracker) {
+        removeMessages(MSG_UPDATE_BATCH_INPUT, tracker);
+    }
+
+    @Override
+    public void cancelAllUpdateBatchInputTimers() {
+        removeMessages(MSG_UPDATE_BATCH_INPUT);
+    }
+
+    public void cancelAllMessages() {
+        cancelKeyTimers();
+        cancelAllUpdateBatchInputTimers();
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
index 51ae636..4fa682d 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
@@ -125,6 +125,7 @@
     private static native boolean needsToRunGCNative(long dict, boolean mindsBlockByGC);
     private static native void flushWithGCNative(long dict, String filePath);
     private static native void closeNative(long dict);
+    private static native int getFormatVersionNative(long dict);
     private static native int getProbabilityNative(long dict, int[] word);
     private static native int getBigramProbabilityNative(long dict, int[] word0, int[] word1);
     private static native int getSuggestionsNative(long dict, long proximityInfo,
@@ -241,6 +242,10 @@
         return mNativeDict != 0;
     }
 
+    public int getFormatVersion() {
+        return getFormatVersionNative(mNativeDict);
+    }
+
     public static float calcNormalizedScore(final String before, final String after,
             final int score) {
         return calcNormalizedScoreNative(StringUtils.toCodePointArray(before),
diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
index 7041df5..7107076 100644
--- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
@@ -28,6 +28,7 @@
 import com.android.inputmethod.latin.utils.AsyncResultHolder;
 import com.android.inputmethod.latin.utils.CollectionUtils;
 import com.android.inputmethod.latin.utils.PrioritizedSerialExecutor;
+import com.android.inputmethod.latin.utils.StringUtils;
 
 import java.io.File;
 import java.util.ArrayList;
@@ -128,6 +129,14 @@
      */
     protected abstract boolean hasContentChanged();
 
+    protected boolean isValidBinaryDictFormatVersion(final int formatVersion) {
+        return true;
+    }
+
+    protected String getFileNameExtentionToOpenDict() {
+        return "";
+    }
+
     /**
      * Gets the dictionary update controller for the given filename.
      */
@@ -238,12 +247,18 @@
             public void run() {
                 if (mDictionaryWriter == null) {
                     mBinaryDictionary.close();
-                    final File file = new File(mContext.getFilesDir(), mFilename + "/"
-                            + FormatSpec.TRIE_FILE_EXTENSION);
+                    final File file = new File(mContext.getFilesDir(), mFilename);
+                    file.delete();
                     BinaryDictionary.createEmptyDictFile(file.getAbsolutePath(),
                             DICTIONARY_FORMAT_VERSION, getHeaderAttributeMap());
+                    // We have 'fileToOpen' in addition to 'file' for the v4 dictionary format
+                    // where 'file' is a directory, and 'fileToOpen' is a normal file.
+                    final File fileToOpen = new File(mContext.getFilesDir(), mFilename
+                            + getFileNameExtentionToOpenDict());
+                    // TODO: Make BinaryDictionary's constructor be able to accept filename
+                    // without extension.
                     mBinaryDictionary = new BinaryDictionary(
-                            file.getAbsolutePath(), 0 /* offset */, file.length(),
+                            fileToOpen.getAbsolutePath(), 0 /* offset */, fileToOpen.length(),
                             true /* useFullEditDistance */, null, mDictType, mIsUpdatable);
                 } else {
                     mDictionaryWriter.clear();
@@ -482,8 +497,8 @@
                     + mFilenameDictionaryUpdateController.mLastUpdateTime);
         }
 
-        final File file = new File(mContext.getFilesDir(), mFilename + "/"
-                + FormatSpec.TRIE_FILE_EXTENSION);
+        final File file = new File(mContext.getFilesDir(), mFilename
+                + getFileNameExtentionToOpenDict());
         final String filename = file.getAbsolutePath();
         final long length = file.length();
 
@@ -526,8 +541,10 @@
             loadDictionaryAsync();
             mDictionaryWriter.write(mFilename, getHeaderAttributeMap());
         } else {
-            if (mBinaryDictionary == null || !mBinaryDictionary.isValidDictionary()) {
+            if (mBinaryDictionary == null || !mBinaryDictionary.isValidDictionary()
+                    || !isValidBinaryDictFormatVersion(mBinaryDictionary.getFormatVersion())) {
                 final File file = new File(mContext.getFilesDir(), mFilename);
+                file.delete();
                 BinaryDictionary.createEmptyDictFile(file.getAbsolutePath(),
                         DICTIONARY_FORMAT_VERSION, getHeaderAttributeMap());
             } else {
@@ -623,8 +640,11 @@
                         // load the shared dictionary.
                         loadBinaryDictionary();
                     }
-                    if (mBinaryDictionary != null && !mBinaryDictionary.isValidDictionary()) {
-                        // Binary dictionary is not valid. Regenerate the dictionary file.
+                    if (mBinaryDictionary != null && !(mBinaryDictionary.isValidDictionary()
+                            && isValidBinaryDictFormatVersion(
+                                    mBinaryDictionary.getFormatVersion()))) {
+                        // Binary dictionary or its format version is not valid. Regenerate the
+                        // dictionary file.
                         mFilenameDictionaryUpdateController.mLastUpdateTime = time;
                         writeBinaryDictionary();
                         loadBinaryDictionary();
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index d14066c..6445b61 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -1779,9 +1779,9 @@
         mInputUpdater.onStartBatchInput();
         mHandler.cancelUpdateSuggestionStrip();
         mConnection.beginBatchEdit();
-        final SettingsValues settingsValues = mSettings.getCurrent();
+        final SettingsValues currentSettingsValues = mSettings.getCurrent();
         if (mWordComposer.isComposingWord()) {
-            if (settingsValues.mIsInternal) {
+            if (currentSettingsValues.mIsInternal) {
                 if (mWordComposer.isBatchMode()) {
                     LatinImeLoggerUtils.onAutoCorrection(
                             "", mWordComposer.getTypedWord(), " ", mWordComposer);
@@ -1808,12 +1808,14 @@
         }
         final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor();
         if (Character.isLetterOrDigit(codePointBeforeCursor)
-                || settingsValues.isUsuallyFollowedBySpace(codePointBeforeCursor)) {
+                || currentSettingsValues.isUsuallyFollowedBySpace(codePointBeforeCursor)) {
             mSpaceState = SPACE_STATE_PHANTOM;
         }
         mConnection.endBatchEdit();
         mKeyboardSwitcher.updateShiftState();
-        mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode());
+        mWordComposer.setCapitalizedModeAndPreviousWordAtStartComposingTime(getActualCapsMode(),
+                // Prev word is 1st word before cursor
+                getNthPreviousWordForSuggestion(currentSettingsValues, 1 /* nthPreviousWord */));
     }
 
     static final class InputUpdater implements Handler.Callback {
@@ -1986,7 +1988,8 @@
                     mConnection.commitText(commitParts[0], 0);
                     mSpaceState = SPACE_STATE_PHANTOM;
                     mKeyboardSwitcher.updateShiftState();
-                    mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode());
+                    mWordComposer.setCapitalizedModeAndPreviousWordAtStartComposingTime(
+                            getActualCapsMode(), commitParts[0]);
                     ++mAutoCommitSequenceNumber;
                 }
             }
@@ -2295,7 +2298,11 @@
             mWordComposer.add(primaryCode, keyX, keyY);
             // If it's the first letter, make note of auto-caps state
             if (mWordComposer.size() == 1) {
-                mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode());
+                // We pass 1 to getPreviousWordForSuggestion because we were not composing a word
+                // yet, so the word we want is the 1st word before the cursor.
+                mWordComposer.setCapitalizedModeAndPreviousWordAtStartComposingTime(
+                        getActualCapsMode(),
+                        getNthPreviousWordForSuggestion(currentSettings, 1 /* nthPreviousWord */));
             }
             mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
         } else {
@@ -2537,12 +2544,18 @@
         }
     }
 
-    private String getPreviousWordForSuggestion(final SettingsValues currentSettings) {
+    /**
+     * Get the nth previous word before the cursor as context for the suggestion process.
+     * @param currentSettings the current settings values.
+     * @param nthPreviousWord reverse index of the word to get (1-indexed)
+     * @return the nth previous word before the cursor.
+     */
+    private String getNthPreviousWordForSuggestion(final SettingsValues currentSettings,
+            final int nthPreviousWord) {
         if (currentSettings.mCurrentLanguageHasSpaces) {
             // If we are typing in a language with spaces we can just look up the previous
             // word from textview.
-            return mConnection.getNthPreviousWord(currentSettings,
-                    mWordComposer.isComposingWord() ? 2 : 1);
+            return mConnection.getNthPreviousWord(currentSettings, nthPreviousWord);
         } else {
             return LastComposedWord.NOT_A_COMPOSED_WORD == mLastComposedWord ? null
                     : mLastComposedWord.mCommittedWord;
@@ -2562,8 +2575,31 @@
         // should just skip whitespace if any, so 1.
         final SettingsValues currentSettings = mSettings.getCurrent();
         final int[] additionalFeaturesOptions = currentSettings.mAdditionalFeaturesSettingValues;
-        final String prevWord = getPreviousWordForSuggestion(currentSettings);
-        suggest.getSuggestedWords(mWordComposer, prevWord, keyboard.getProximityInfo(),
+
+        final String previousWord;
+        if (mWordComposer.isComposingWord() || mWordComposer.isBatchMode()) {
+            previousWord = mWordComposer.getPreviousWord();
+        } else {
+            // Not composing: this is for prediction.
+            // TODO: read the previous word earlier for prediction, like we are doing for
+            // normal suggestions.
+            previousWord = getNthPreviousWordForSuggestion(currentSettings, 1 /* nthPreviousWord*/);
+        }
+        if (DEBUG) {
+            // TODO: this is for checking consistency with older versions. Remove this when
+            // we are confident this is stable.
+            // We're checking the previous word in the text field against the memorized previous
+            // word. If we are composing a word we should have the second word before the cursor
+            // memorized, otherwise we should have the first.
+            final String rereadPrevWord = getNthPreviousWordForSuggestion(currentSettings,
+                    mWordComposer.isComposingWord() ? 2 : 1);
+            if (!TextUtils.equals(previousWord, rereadPrevWord)) {
+                throw new RuntimeException("Unexpected previous word: "
+                        + previousWord + " <> " + rereadPrevWord);
+            }
+        }
+        suggest.getSuggestedWords(mWordComposer, mWordComposer.getPreviousWord(),
+                keyboard.getProximityInfo(),
                 currentSettings.mBlockPotentiallyOffensive, currentSettings.mCorrectionEnabled,
                 additionalFeaturesOptions, sessionId, sequenceNumber, callback);
     }
@@ -2900,7 +2936,13 @@
                 }
             }
         }
-        mWordComposer.setComposingWord(typedWord, mKeyboardSwitcher.getKeyboard());
+        mWordComposer.setComposingWord(typedWord,
+                getNthPreviousWordForSuggestion(currentSettings,
+                        // We want the previous word for suggestion. If we have chars in the word
+                        // before the cursor, then we want the word before that, hence 2; otherwise,
+                        // we want the word immediately before the cursor, hence 1.
+                        0 == numberOfCharsInWordBeforeCursor ? 1 : 2),
+                mKeyboardSwitcher.getKeyboard());
         mWordComposer.setCursorPositionWithinWord(
                 typedWord.codePointCount(0, numberOfCharsInWordBeforeCursor));
         mConnection.setComposingRegion(
@@ -2978,7 +3020,11 @@
     }
 
     private void restartSuggestionsOnWordBeforeCursor(final String word) {
-        mWordComposer.setComposingWord(word, mKeyboardSwitcher.getKeyboard());
+        mWordComposer.setComposingWord(word,
+                // Previous word is the 2nd word before cursor because we are restarting on the
+                // 1st word before cursor.
+                getNthPreviousWordForSuggestion(mSettings.getCurrent(), 2 /* nthPreviousWord */),
+                mKeyboardSwitcher.getKeyboard());
         final int length = word.length();
         mConnection.deleteSurroundingText(length, 0);
         mConnection.setComposingText(word, 1);
@@ -3044,7 +3090,8 @@
         } else {
             // For languages without spaces, we revert the typed string but the cursor is flush
             // with the typed word, so we need to resume suggestions right away.
-            mWordComposer.setComposingWord(stringToCommit, mKeyboardSwitcher.getKeyboard());
+            mWordComposer.setComposingWord(stringToCommit, previousWord,
+                    mKeyboardSwitcher.getKeyboard());
             mConnection.setComposingText(stringToCommit, 1);
         }
         if (mSettings.isInternal()) {
diff --git a/java/src/com/android/inputmethod/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java
index 039dadc..2f81d15 100644
--- a/java/src/com/android/inputmethod/latin/WordComposer.java
+++ b/java/src/com/android/inputmethod/latin/WordComposer.java
@@ -48,6 +48,10 @@
     // at any given time. However this is not limited in size, while mPrimaryKeyCodes is limited
     // to MAX_WORD_LENGTH code points.
     private final StringBuilder mTypedWord;
+    // The previous word (before the composing word). Used as context for suggestions. May be null
+    // after resetting and before starting a new composing word, or when there is no context like
+    // at the start of text for example.
+    private String mPreviousWord;
     private String mAutoCorrection;
     private boolean mIsResumed;
     private boolean mIsBatchMode;
@@ -85,6 +89,7 @@
         mIsBatchMode = false;
         mCursorPositionWithinWord = 0;
         mRejectedBatchModeSuggestion = null;
+        mPreviousWord = null;
         refreshSize();
     }
 
@@ -101,6 +106,7 @@
         mIsBatchMode = source.mIsBatchMode;
         mCursorPositionWithinWord = source.mCursorPositionWithinWord;
         mRejectedBatchModeSuggestion = source.mRejectedBatchModeSuggestion;
+        mPreviousWord = source.mPreviousWord;
         refreshSize();
     }
 
@@ -118,6 +124,7 @@
         mIsBatchMode = false;
         mCursorPositionWithinWord = 0;
         mRejectedBatchModeSuggestion = null;
+        mPreviousWord = null;
         refreshSize();
     }
 
@@ -284,8 +291,13 @@
     /**
      * Set the currently composing word to the one passed as an argument.
      * This will register NOT_A_COORDINATE for X and Ys, and use the passed keyboard for proximity.
+     * @param word the char sequence to set as the composing word.
+     * @param previousWord the previous word, to use as context for suggestions. Can be null if
+     *   the context is nil (typically, at start of text).
+     * @param keyboard the keyboard this is typed on, for coordinate info/proximity.
      */
-    public void setComposingWord(final CharSequence word, final Keyboard keyboard) {
+    public void setComposingWord(final CharSequence word, final String previousWord,
+            final Keyboard keyboard) {
         reset();
         final int length = word.length();
         for (int i = 0; i < length; i = Character.offsetByCodePoints(word, i, 1)) {
@@ -293,6 +305,7 @@
             addKeyInfo(codePoint, keyboard);
         }
         mIsResumed = true;
+        mPreviousWord = previousWord;
     }
 
     /**
@@ -343,6 +356,10 @@
         return mTypedWord.toString();
     }
 
+    public String getPreviousWord() {
+        return mPreviousWord;
+    }
+
     /**
      * Whether or not the user typed a capital letter as the first letter in the word
      * @return capitalization preference
@@ -388,18 +405,21 @@
     }
 
     /**
-     * Saves the caps mode at the start of composing.
+     * Saves the caps mode and the previous word at the start of composing.
      *
-     * WordComposer needs to know about this for several reasons. The first is, we need to know
-     * after the fact what the reason was, to register the correct form into the user history
-     * dictionary: if the word was automatically capitalized, we should insert it in all-lower
-     * case but if it's a manual pressing of shift, then it should be inserted as is.
+     * WordComposer needs to know about the caps mode for several reasons. The first is, we need
+     * to know after the fact what the reason was, to register the correct form into the user
+     * history dictionary: if the word was automatically capitalized, we should insert it in
+     * all-lower case but if it's a manual pressing of shift, then it should be inserted as is.
      * Also, batch input needs to know about the current caps mode to display correctly
      * capitalized suggestions.
      * @param mode the mode at the time of start
+     * @param previousWord the previous word as context for suggestions. May be null if none.
      */
-    public void setCapitalizedModeAtStartComposingTime(final int mode) {
+    public void setCapitalizedModeAndPreviousWordAtStartComposingTime(final int mode,
+            final String previousWord) {
         mCapitalizedMode = mode;
+        mPreviousWord = previousWord;
     }
 
     /**
@@ -451,6 +471,7 @@
         mCapsCount = 0;
         mDigitsCount = 0;
         mIsBatchMode = false;
+        mPreviousWord = mTypedWord.toString();
         mTypedWord.setLength(0);
         mCodePointSize = 0;
         mTrailingSingleQuotesCount = 0;
@@ -464,7 +485,8 @@
         return lastComposedWord;
     }
 
-    public void resumeSuggestionOnLastComposedWord(final LastComposedWord lastComposedWord) {
+    public void resumeSuggestionOnLastComposedWord(final LastComposedWord lastComposedWord,
+            final String previousWord) {
         mPrimaryKeyCodes = lastComposedWord.mPrimaryKeyCodes;
         mInputPointers.set(lastComposedWord.mInputPointers);
         mTypedWord.setLength(0);
@@ -475,6 +497,7 @@
         mCursorPositionWithinWord = mCodePointSize;
         mRejectedBatchModeSuggestion = null;
         mIsResumed = true;
+        mPreviousWord = previousWord;
     }
 
     public boolean isBatchMode() {
diff --git a/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java b/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java
index bc11601..8b94883 100644
--- a/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java
+++ b/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java
@@ -56,6 +56,8 @@
     public static final int FREQUENCY_FOR_WORDS_IN_DICTS = FREQUENCY_FOR_TYPED;
     public static final int FREQUENCY_FOR_WORDS_NOT_IN_DICTS = Dictionary.NOT_A_PROBABILITY;
 
+    public static final int REQUIRED_BINARY_DICTIONARY_VERSION = 4;
+
     /** Locale for which this user history dictionary is storing words */
     private final String mLocale;
 
@@ -114,6 +116,16 @@
         return false;
     }
 
+    @Override
+    protected boolean isValidBinaryDictFormatVersion(final int formatVersion) {
+        return formatVersion >= REQUIRED_BINARY_DICTIONARY_VERSION;
+    }
+
+    @Override
+    protected String getFileNameExtentionToOpenDict() {
+        return "/" + FormatSpec.TRIE_FILE_EXTENSION;
+    }
+
     public void addMultipleDictionaryEntriesToDictionary(
             final ArrayList<LanguageModelParam> languageModelParams,
             final ExpandableBinaryDictionary.AddMultipleDictionaryEntriesCallback callback) {
diff --git a/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp b/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
index 81997b7..c6a5900 100644
--- a/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
+++ b/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
@@ -135,6 +135,12 @@
     delete dictionary;
 }
 
+static int latinime_BinaryDictionary_getFormatVersion(JNIEnv *env, jclass clazz, jlong dict) {
+    Dictionary *dictionary = reinterpret_cast<Dictionary *>(dict);
+    if (!dictionary) return 0;
+    return dictionary->getFormatVersionNumber();
+}
+
 static int latinime_BinaryDictionary_getSuggestions(JNIEnv *env, jclass clazz, jlong dict,
         jlong proximityInfo, jlong dicTraverseSession, jintArray xCoordinatesArray,
         jintArray yCoordinatesArray, jintArray timesArray, jintArray pointerIdsArray,
@@ -432,6 +438,11 @@
         reinterpret_cast<void *>(latinime_BinaryDictionary_close)
     },
     {
+        const_cast<char *>("getFormatVersionNative"),
+        const_cast<char *>("(J)I"),
+        reinterpret_cast<void *>(latinime_BinaryDictionary_getFormatVersion)
+    },
+    {
         const_cast<char *>("flushNative"),
         const_cast<char *>("(JLjava/lang/String;)V"),
         reinterpret_cast<void *>(latinime_BinaryDictionary_flush)
diff --git a/native/jni/src/suggest/core/dictionary/dictionary.h b/native/jni/src/suggest/core/dictionary/dictionary.h
index 8862b2b..0bdb92d 100644
--- a/native/jni/src/suggest/core/dictionary/dictionary.h
+++ b/native/jni/src/suggest/core/dictionary/dictionary.h
@@ -22,6 +22,7 @@
 #include "defines.h"
 #include "jni.h"
 #include "suggest/core/dictionary/bigram_dictionary.h"
+#include "suggest/core/policy/dictionary_header_structure_policy.h"
 #include "suggest/core/policy/dictionary_structure_with_buffer_policy.h"
 #include "suggest/core/suggest_interface.h"
 #include "utils/exclusive_ownership_pointer.h"
@@ -93,6 +94,11 @@
         return mDictionaryStructureWithBufferPolicy.get();
     }
 
+    int getFormatVersionNumber() const {
+        return mDictionaryStructureWithBufferPolicy.get()->getHeaderStructurePolicy()
+                ->getFormatVersionNumber();
+    }
+
  private:
     DISALLOW_IMPLICIT_CONSTRUCTORS(Dictionary);
 
diff --git a/native/jni/src/suggest/core/policy/dictionary_header_structure_policy.h b/native/jni/src/suggest/core/policy/dictionary_header_structure_policy.h
index 5492c60..b05b7c3 100644
--- a/native/jni/src/suggest/core/policy/dictionary_header_structure_policy.h
+++ b/native/jni/src/suggest/core/policy/dictionary_header_structure_policy.h
@@ -29,6 +29,8 @@
  public:
     virtual ~DictionaryHeaderStructurePolicy() {}
 
+    virtual int getFormatVersionNumber() const = 0;
+
     virtual bool supportsDynamicUpdate() const = 0;
 
     virtual bool requiresGermanUmlautProcessing() const = 0;
diff --git a/native/jni/src/suggest/policyimpl/dictionary/header/header_policy.h b/native/jni/src/suggest/policyimpl/dictionary/header/header_policy.h
index 8161249..9726fb8 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/header/header_policy.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/header/header_policy.h
@@ -70,6 +70,19 @@
 
     ~HeaderPolicy() {}
 
+    virtual int getFormatVersionNumber() const {
+        switch (mDictFormatVersion) {
+            case FormatUtils::VERSION_2:
+                return 2;
+            case FormatUtils::VERSION_3:
+                return 3;
+            case FormatUtils::VERSION_4:
+                return 4;
+            default:
+                return 0;
+        }
+    }
+
     AK_FORCE_INLINE int getSize() const {
         return mSize;
     }
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_dict_buffers.cpp b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_dict_buffers.cpp
index d312531..40c9b5e 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_dict_buffers.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_dict_buffers.cpp
@@ -34,6 +34,13 @@
     FileUtils::getFilePathWithSuffix(dictDirPath,
             DictFileWritingUtils::TEMP_FILE_SUFFIX_FOR_WRITING_DICT_FILE, tmpDirPathBufSize,
             tmpDirPath);
+    if (FileUtils::existsDir(tmpDirPath)) {
+        if (!FileUtils::removeDirAndFiles(tmpDirPath)) {
+            AKLOGE("Existing directory %s cannot be removed.", tmpDirPath);
+            ASSERT(false);
+            return false;
+        }
+    }
     if (mkdir(tmpDirPath, S_IRWXU) == -1) {
         AKLOGE("Cannot create directory: %s. errno: %d.", tmpDirPath, errno);
         return false;
diff --git a/native/jni/src/suggest/policyimpl/dictionary/utils/file_utils.cpp b/native/jni/src/suggest/policyimpl/dictionary/utils/file_utils.cpp
index dedcd7a..34da769 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/utils/file_utils.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/utils/file_utils.cpp
@@ -41,6 +41,15 @@
     return static_cast<int>(statBuf.st_size);
 }
 
+/* static */ bool FileUtils::existsDir(const char *const dirPath) {
+    DIR *const dir = opendir(dirPath);
+    if (dir == NULL) {
+        return false;
+    }
+    closedir(dir);
+    return true;
+}
+
 // Remove a directory and all files in the directory.
 /* static */ bool FileUtils::removeDirAndFiles(const char *const dirPath) {
     DIR *const dir = opendir(dirPath);
@@ -58,9 +67,11 @@
         getFilePath(dirPath, dirent->d_name, filePathBufSize, filePath);
         if (remove(filePath) != 0) {
             AKLOGE("Cannot remove file %s.", filePath);
+            closedir(dir);
             return false;
         }
     }
+    closedir(dir);
     if (remove(dirPath) != 0) {
         AKLOGE("Cannot remove directory %s.", dirPath);
         return false;
diff --git a/native/jni/src/suggest/policyimpl/dictionary/utils/file_utils.h b/native/jni/src/suggest/policyimpl/dictionary/utils/file_utils.h
index 7dcdef8..e558373 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/utils/file_utils.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/utils/file_utils.h
@@ -26,6 +26,8 @@
     // Returns -1 on error.
     static int getFileSize(const char *const filePath);
 
+    static bool existsDir(const char *const dirPath);
+
     // Remove a directory and all files in the directory.
     static bool removeDirAndFiles(const char *const dirPath);
 
diff --git a/tests/src/com/android/inputmethod/latin/WordComposerTests.java b/tests/src/com/android/inputmethod/latin/WordComposerTests.java
index 1434c6b..a67f6a4 100644
--- a/tests/src/com/android/inputmethod/latin/WordComposerTests.java
+++ b/tests/src/com/android/inputmethod/latin/WordComposerTests.java
@@ -26,8 +26,15 @@
 public class WordComposerTests extends AndroidTestCase {
     public void testMoveCursor() {
         final WordComposer wc = new WordComposer();
+        // BMP is the Basic Multilingual Plane, as defined by Unicode. This includes
+        // most characters for most scripts, including all Roman alphabet languages,
+        // CJK, Arabic, Hebrew. Notable exceptions include some emoji and some
+        // very rare Chinese ideograms. BMP characters can be encoded on 2 bytes
+        // in UTF-16, whereas those outside the BMP need 4 bytes.
+        // http://en.wikipedia.org/wiki/Plane_(Unicode)#Basic_Multilingual_Plane
         final String STR_WITHIN_BMP = "abcdef";
-        wc.setComposingWord(STR_WITHIN_BMP, null);
+        final String PREVWORD = "prevword";
+        wc.setComposingWord(STR_WITHIN_BMP, PREVWORD, null /* keyboard */);
         assertEquals(wc.size(),
                 STR_WITHIN_BMP.codePointCount(0, STR_WITHIN_BMP.length()));
         assertFalse(wc.isCursorFrontOrMiddleOfComposingWord());
@@ -43,13 +50,20 @@
         // Move the cursor to after the 'f'
         assertTrue(wc.moveCursorByAndReturnIfInsideComposingWord(1));
         assertFalse(wc.isCursorFrontOrMiddleOfComposingWord());
+        // Check the previous word is still there
+        assertEquals(PREVWORD, wc.getPreviousWord());
         // Move the cursor past the end of the word
         assertFalse(wc.moveCursorByAndReturnIfInsideComposingWord(1));
         assertFalse(wc.moveCursorByAndReturnIfInsideComposingWord(15));
+        // Do what LatinIME does when the cursor is moved outside of the word,
+        // and check the behavior is correct.
+        wc.reset();
+        assertNull(wc.getPreviousWord());
 
         // \uD861\uDED7 is 𨛗, a character outside the BMP
         final String STR_WITH_SUPPLEMENTARY_CHAR = "abcde\uD861\uDED7fgh";
-        wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, null);
+        wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, null /* previousWord */,
+                null /* keyboard */);
         assertEquals(wc.size(), STR_WITH_SUPPLEMENTARY_CHAR.codePointCount(0,
                         STR_WITH_SUPPLEMENTARY_CHAR.length()));
         assertFalse(wc.isCursorFrontOrMiddleOfComposingWord());
@@ -59,34 +73,46 @@
         assertTrue(wc.isCursorFrontOrMiddleOfComposingWord());
         assertTrue(wc.moveCursorByAndReturnIfInsideComposingWord(1));
         assertFalse(wc.isCursorFrontOrMiddleOfComposingWord());
+        assertNull(wc.getPreviousWord());
 
-        wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, null);
+        wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, STR_WITHIN_BMP, null /* keyboard */);
         wc.setCursorPositionWithinWord(3);
         assertTrue(wc.moveCursorByAndReturnIfInsideComposingWord(7));
+        assertEquals(STR_WITHIN_BMP, wc.getPreviousWord());
 
-        wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, null);
+        wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, STR_WITH_SUPPLEMENTARY_CHAR,
+                null /* keyboard */);
         wc.setCursorPositionWithinWord(3);
         assertTrue(wc.moveCursorByAndReturnIfInsideComposingWord(7));
+        assertEquals(STR_WITH_SUPPLEMENTARY_CHAR, wc.getPreviousWord());
 
-        wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, null);
+        wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, STR_WITHIN_BMP, null /* keyboard */);
         wc.setCursorPositionWithinWord(3);
         assertTrue(wc.moveCursorByAndReturnIfInsideComposingWord(-3));
         assertFalse(wc.moveCursorByAndReturnIfInsideComposingWord(-1));
+        assertEquals(STR_WITHIN_BMP, wc.getPreviousWord());
 
-        wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, null);
+        wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, null /* previousWord */,
+                null /* keyboard */);
         wc.setCursorPositionWithinWord(3);
         assertFalse(wc.moveCursorByAndReturnIfInsideComposingWord(-9));
+        assertNull(wc.getPreviousWord());
 
-        wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, null);
+        wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, STR_WITH_SUPPLEMENTARY_CHAR,
+                null /* keyboard */);
         assertTrue(wc.moveCursorByAndReturnIfInsideComposingWord(-10));
+        assertEquals(STR_WITH_SUPPLEMENTARY_CHAR, wc.getPreviousWord());
 
-        wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, null);
+        wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, null /* previousWord */,
+                null /* keyboard */);
         assertFalse(wc.moveCursorByAndReturnIfInsideComposingWord(-11));
 
-        wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, null);
+        wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, null /* previousWord */,
+                null /* keyboard */);
         assertTrue(wc.moveCursorByAndReturnIfInsideComposingWord(0));
 
-        wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, null);
+        wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, null /* previousWord */,
+                null /* keyboard */);
         wc.setCursorPositionWithinWord(2);
         assertTrue(wc.moveCursorByAndReturnIfInsideComposingWord(0));
     }