diff --git a/java-overridable/src/com/android/inputmethod/latin/touchinputconsumer/GestureConsumer.java b/java-overridable/src/com/android/inputmethod/latin/touchinputconsumer/GestureConsumer.java
new file mode 100644
index 0000000000000000000000000000000000000000..672d6d1a1fbaa05b318885aa7da15e1625beb1a1
--- /dev/null
+++ b/java-overridable/src/com/android/inputmethod/latin/touchinputconsumer/GestureConsumer.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2014 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.touchinputconsumer;
+
+import android.view.inputmethod.EditorInfo;
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.latin.InputPointers;
+import com.android.inputmethod.latin.SuggestedWords;
+import com.android.inputmethod.latin.inputlogic.PrivateCommandPerformer;
+
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Stub for GestureConsumer.
+ * <br>
+ * The methods of this class should only be called from a single thread, e.g.,
+ * the UI Thread.
+ */
+public class GestureConsumer {
+    public static final GestureConsumer NULL_GESTURE_CONSUMER =
+            new GestureConsumer();
+
+    public static GestureConsumer newInstance(
+            final EditorInfo editorInfo, final PrivateCommandPerformer commandPerformer,
+            final List<Locale> locales, final Keyboard keyboard) {
+        return GestureConsumer.NULL_GESTURE_CONSUMER;
+    }
+
+    private GestureConsumer() {
+    }
+
+    public boolean willConsume() {
+        return false;
+    }
+
+    public void onInit(final List<Locale> locales, final Keyboard keyboard) {
+    }
+
+    public void onGestureStarted(final List<Locale> locales, final Keyboard keyboard) {
+    }
+
+    public void onGestureCanceled() {
+    }
+
+    public void onGestureCompleted(final InputPointers inputPointers) {
+    }
+
+    public void onImeSuggestionsProcessed(final SuggestedWords suggestedWords,
+            final int composingStart, final int composingLength) {
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 714f62f3ec1d24c17826c93c74ae583985fd89ab..861a8903e834a87c85b01c26be91a8038169074c 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -84,6 +84,7 @@ import com.android.inputmethod.latin.settings.SettingsActivity;
 import com.android.inputmethod.latin.settings.SettingsValues;
 import com.android.inputmethod.latin.suggestions.SuggestionStripView;
 import com.android.inputmethod.latin.suggestions.SuggestionStripViewAccessor;
+import com.android.inputmethod.latin.touchinputconsumer.GestureConsumer;
 import com.android.inputmethod.latin.utils.ApplicationUtils;
 import com.android.inputmethod.latin.utils.CapsModeUtils;
 import com.android.inputmethod.latin.utils.CoordinateUtils;
@@ -101,6 +102,7 @@ import com.android.inputmethod.latin.utils.ViewLayoutUtils;
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Locale;
 import java.util.concurrent.TimeUnit;
@@ -176,6 +178,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
 
     private final boolean mIsHardwareAcceleratedDrawingEnabled;
 
+    private GestureConsumer mGestureConsumer = GestureConsumer.NULL_GESTURE_CONSUMER;
+
     public final UIHandler mHandler = new UIHandler(this);
 
     public static final class UIHandler extends LeakGuardHandlerWrapper<LatinIME> {
@@ -255,9 +259,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
                 latinIme.resetDictionaryFacilitatorIfNecessary();
                 break;
             case MSG_UPDATE_TAIL_BATCH_INPUT_COMPLETED:
+                final SuggestedWords suggestedWords = (SuggestedWords) msg.obj;
                 latinIme.mInputLogic.onUpdateTailBatchInputCompleted(
                         latinIme.mSettings.getCurrent(),
-                        (SuggestedWords) msg.obj, latinIme.mKeyboardSwitcher);
+                        suggestedWords, latinIme.mKeyboardSwitcher);
+                latinIme.onTailBatchInputResultShown(suggestedWords);
                 break;
             case MSG_RESET_CACHES:
                 final SettingsValues settingsValues = latinIme.mSettings.getCurrent();
@@ -799,6 +805,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
         StatsUtils.onFinishInputView();
         mHandler.onFinishInputView(finishingInput);
         mStatsUtilsManager.onFinishInputView();
+        mGestureConsumer = GestureConsumer.NULL_GESTURE_CONSUMER;
     }
 
     @Override
@@ -824,6 +831,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
     @SuppressWarnings("deprecation")
     private void onStartInputViewInternal(final EditorInfo editorInfo, final boolean restarting) {
         super.onStartInputView(editorInfo, restarting);
+        // Switch to the null consumer to handle cases leading to early exit below, for which we
+        // also wouldn't be consuming gesture data.
+        mGestureConsumer = GestureConsumer.NULL_GESTURE_CONSUMER;
         mRichImm.clearSubtypeCaches();
         final KeyboardSwitcher switcher = mKeyboardSwitcher;
         switcher.updateKeyboardTheme();
@@ -867,6 +877,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
             return;
         }
 
+        // Update to a gesture consumer with the current editor and IME state.
+        mGestureConsumer = GestureConsumer.newInstance(editorInfo,
+                mInputLogic.getPrivateCommandPerformer(),
+                Collections.singletonList(mSubtypeSwitcher.getCurrentSubtypeLocale()),
+                switcher.getKeyboard());
+
         // Forward this event to the accessibility utilities, if enabled.
         final AccessibilityUtils accessUtils = AccessibilityUtils.getInstance();
         if (accessUtils.isTouchExplorationEnabled()) {
@@ -1397,6 +1413,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
     @Override
     public void onStartBatchInput() {
         mInputLogic.onStartBatchInput(mSettings.getCurrent(), mKeyboardSwitcher, mHandler);
+        mGestureConsumer.onGestureStarted(
+                Collections.singletonList(mSubtypeSwitcher.getCurrentSubtypeLocale()),
+                mKeyboardSwitcher.getKeyboard());
     }
 
     @Override
@@ -1407,11 +1426,25 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
     @Override
     public void onEndBatchInput(final InputPointers batchPointers) {
         mInputLogic.onEndBatchInput(batchPointers);
+        mGestureConsumer.onGestureCompleted(batchPointers);
     }
 
     @Override
     public void onCancelBatchInput() {
         mInputLogic.onCancelBatchInput(mHandler);
+        mGestureConsumer.onGestureCanceled();
+    }
+
+    /**
+     * To be called after the InputLogic has gotten a chance to act on the on-device decoding
+     * for the full gesture, possibly updating the TextView to reflect the first decoding.
+     * <p>
+     * This method must be run on the UI Thread.
+     * @param suggestedWords On-device decoding for the full gesture.
+     */
+    public void onTailBatchInputResultShown(final SuggestedWords suggestedWords) {
+        mGestureConsumer.onImeSuggestionsProcessed(suggestedWords,
+                mInputLogic.getComposingStart(), mInputLogic.getComposingLength());
     }
 
     // This method must run on the UI Thread.
diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java
index a7ea2a1c85a963606e928dc741bb9a139da17b6e..62a258b20ede09d0066ebb2b1a6dcfcda80aaaea 100644
--- a/java/src/com/android/inputmethod/latin/RichInputConnection.java
+++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java
@@ -19,6 +19,7 @@ package com.android.inputmethod.latin;
 import android.graphics.Color;
 import android.inputmethodservice.InputMethodService;
 import android.os.Build;
+import android.os.Bundle;
 import android.text.SpannableStringBuilder;
 import android.text.Spanned;
 import android.text.TextUtils;
@@ -33,6 +34,7 @@ import android.view.inputmethod.InputConnection;
 import android.view.inputmethod.InputMethodManager;
 
 import com.android.inputmethod.compat.InputConnectionCompatUtils;
+import com.android.inputmethod.latin.inputlogic.PrivateCommandPerformer;
 import com.android.inputmethod.latin.settings.SpacingAndPunctuations;
 import com.android.inputmethod.latin.utils.CapsModeUtils;
 import com.android.inputmethod.latin.utils.DebugLogUtils;
@@ -52,7 +54,7 @@ import java.util.Arrays;
  * all the time to find out what text is in the buffer, when we need it to determine caps mode
  * for example.
  */
-public final class RichInputConnection {
+public final class RichInputConnection implements PrivateCommandPerformer {
     private static final String TAG = RichInputConnection.class.getSimpleName();
     private static final boolean DBG = false;
     private static final boolean DEBUG_PREVIOUS_TEXT = false;
@@ -896,6 +898,15 @@ public final class RichInputConnection {
         }
     }
 
+    @Override
+    public boolean performPrivateCommand(final String action, final Bundle data) {
+        mIC = mParent.getCurrentInputConnection();
+        if (mIC == null) {
+            return false;
+        }
+        return mIC.performPrivateCommand(action, data);
+    }
+
     public int getExpectedSelectionStart() {
         return mExpectedSelStart;
     }
diff --git a/java/src/com/android/inputmethod/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java
index f85b34b5e4ac5637281440d0f603f9a126e7fd76..157bd15657588cde2de774db860e48a8292c9780 100644
--- a/java/src/com/android/inputmethod/latin/WordComposer.java
+++ b/java/src/com/android/inputmethod/latin/WordComposer.java
@@ -128,8 +128,7 @@ public final class WordComposer {
      * Number of keystrokes in the composing word.
      * @return the number of keystrokes
      */
-    // This may be made public if need be, but right now it's not used anywhere
-    /* package for tests */ int size() {
+    public int size() {
         return mCodePointSize;
     }
 
diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
index b4a1c3e65e66bdb9f5f4f5b4c7b616f0094a0867..1b1d5e7e53397473c40b819f3db3ee82a562c9be 100644
--- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
+++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
@@ -2263,6 +2263,47 @@ public final class InputLogic {
         mConnection.setComposingText(composingTextToBeSet, newCursorPosition);
     }
 
+    /**
+     * Gets an object allowing private IME commands to be sent to the
+     * underlying editor.
+     * @return An object for sending private commands to the underlying editor.
+     */
+    public PrivateCommandPerformer getPrivateCommandPerformer() {
+        return mConnection;
+    }
+
+    /**
+     * Gets the expected index of the first char of the composing span within the editor's text.
+     * Returns a negative value in case there appears to be no valid composing span.
+     *
+     * @see #getComposingLength()
+     * @see RichInputConnection#hasSelection()
+     * @see RichInputConnection#isCursorPositionKnown()
+     * @see RichInputConnection#getExpectedSelectionStart()
+     * @see RichInputConnection#getExpectedSelectionEnd()
+     * @return The expected index in Java chars of the first char of the composing span.
+     */
+    // TODO: try and see if we can get rid of this method. Ideally the users of this class should
+    // never need to know this.
+    public int getComposingStart() {
+        if (!mConnection.isCursorPositionKnown() || mConnection.hasSelection()) {
+            return -1;
+        }
+        return mConnection.getExpectedSelectionStart() - mWordComposer.size();
+    }
+
+    /**
+     * Gets the expected length in Java chars of the composing span.
+     * May be 0 if there is no valid composing span.
+     * @see #getComposingStart()
+     * @return The expected length of the composing span.
+     */
+    // TODO: try and see if we can get rid of this method. Ideally the users of this class should
+    // never need to know this.
+    public int getComposingLength() {
+        return mWordComposer.size();
+    }
+
     //////////////////////////////////////////////////////////////////////////////////////////////
     // Following methods are tentatively placed in this class for the integration with
     // TextDecorator.
diff --git a/java/src/com/android/inputmethod/latin/inputlogic/PrivateCommandPerformer.java b/java/src/com/android/inputmethod/latin/inputlogic/PrivateCommandPerformer.java
new file mode 100644
index 0000000000000000000000000000000000000000..42eaa9c82e59c373f76bc764712ad63d55468647
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/inputlogic/PrivateCommandPerformer.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2014 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.inputlogic;
+
+import android.os.Bundle;
+
+/**
+ * Provides an interface matching
+ * {@link android.view.inputmethod.InputConnection#performPrivateCommand(String,Bundle)}.
+ */
+public interface PrivateCommandPerformer {
+    /**
+     * API to send private commands from an input method to its connected
+     * editor. This can be used to provide domain-specific features that are
+     * only known between certain input methods and their clients.
+     *
+     * @param action Name of the command to be performed. This must be a scoped
+     *            name, i.e. prefixed with a package name you own, so that
+     *            different developers will not create conflicting commands.
+     * @param data Any data to include with the command.
+     * @return true if the command was sent (regardless of whether the
+     * associated editor understood it), false if the input connection is no
+     * longer valid.
+     */
+    boolean performPrivateCommand(String action, Bundle data);
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java b/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java
index 61292fc36fa5b8a283ab3f3c3b6bb9968900d0a3..fb36b7c50144cb7127479d0911a80979b77eec72 100644
--- a/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java
@@ -17,6 +17,7 @@
 package com.android.inputmethod.latin.utils;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Map;
 import java.util.TreeMap;
@@ -40,4 +41,13 @@ public final class CollectionUtils {
         }
         return list;
     }
+
+    /**
+     * Tests whether c contains no elements, true if c is null or c is empty.
+     * @param c Collection to test.
+     * @return Whether c contains no elements.
+     */
+    public static boolean isNullOrEmpty(final Collection c) {
+        return c == null || c.isEmpty();
+    }
 }
diff --git a/tests/src/com/android/inputmethod/latin/touchinputconsumer/NullGestureConsumerTests.java b/tests/src/com/android/inputmethod/latin/touchinputconsumer/NullGestureConsumerTests.java
new file mode 100644
index 0000000000000000000000000000000000000000..ca1039bd9483d261c6a9ccc13c67196071766f42
--- /dev/null
+++ b/tests/src/com/android/inputmethod/latin/touchinputconsumer/NullGestureConsumerTests.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2014 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.touchinputconsumer;
+
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+/**
+ * Tests for GestureConsumer.NULL_GESTURE_CONSUMER.
+ */
+@SmallTest
+public class NullGestureConsumerTests extends AndroidTestCase {
+    /**
+     * Tests that GestureConsumer.NULL_GESTURE_CONSUMER indicates that it won't consume gesture data
+     * and that its methods don't raise exceptions even for invalid data.
+     */
+    public void testNullGestureConsumer() {
+        assertFalse(GestureConsumer.NULL_GESTURE_CONSUMER.willConsume());
+        GestureConsumer.NULL_GESTURE_CONSUMER.onInit(null, null);
+        GestureConsumer.NULL_GESTURE_CONSUMER.onGestureStarted(null, null);
+        GestureConsumer.NULL_GESTURE_CONSUMER.onGestureCanceled();
+        GestureConsumer.NULL_GESTURE_CONSUMER.onGestureCompleted(null);
+        GestureConsumer.NULL_GESTURE_CONSUMER.onImeSuggestionsProcessed(null, -1, -1);
+    }
+
+    /**
+     * Tests that newInstance returns NULL_GESTURE_CONSUMER for invalid input.
+     */
+    public void testNewInstanceGivesNullGestureConsumerForInvalidInputs() {
+        assertSame(GestureConsumer.NULL_GESTURE_CONSUMER,
+                GestureConsumer.newInstance(null, null, null, null));
+    }
+}
diff --git a/tests/src/com/android/inputmethod/latin/utils/CollectionUtilsTests.java b/tests/src/com/android/inputmethod/latin/utils/CollectionUtilsTests.java
new file mode 100644
index 0000000000000000000000000000000000000000..76e28288fd8989066b18577afc24611fe72c8e89
--- /dev/null
+++ b/tests/src/com/android/inputmethod/latin/utils/CollectionUtilsTests.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2014 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.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+
+/**
+ * Tests for {@link CollectionUtils}.
+ */
+@SmallTest
+public class CollectionUtilsTests extends AndroidTestCase {
+    /**
+     * Tests that {@link CollectionUtils#arrayAsList(E[],int,int)} gives the expected
+     * results for a few valid inputs.
+     */
+    public void testArrayAsList() {
+        final String[] array = { "0", "1", "2", "3", "4" };
+        final ArrayList<String> empty = new ArrayList<>();
+        assertEquals(empty, CollectionUtils.arrayAsList(array, 0, 0));
+        assertEquals(empty, CollectionUtils.arrayAsList(array, 1, 1));
+        final ArrayList<String> expected123 = new ArrayList<>(Arrays.asList("1", "2", "3"));
+        assertEquals(expected123, CollectionUtils.arrayAsList(array, 1, 4));
+    }
+
+    /**
+     * Tests that {@link CollectionUtils#isEmpty(java.util.Collection)} gives the expected
+     * results for a few cases.
+     */
+    public void testIsNullOrEmpty() {
+        assertTrue(CollectionUtils.isNullOrEmpty(null));
+        assertTrue(CollectionUtils.isNullOrEmpty(new ArrayList()));
+        assertTrue(CollectionUtils.isNullOrEmpty(Collections.EMPTY_SET));
+        assertFalse(CollectionUtils.isNullOrEmpty(Collections.singleton("Not empty")));
+    }
+
+}