Merge "[B9] Pass the client id parameter for dict info requests"
diff --git a/java/res/layout/research_feedback_fragment_layout.xml b/java/res/layout/research_feedback_fragment_layout.xml
index 2725e7f..2915a77 100644
--- a/java/res/layout/research_feedback_fragment_layout.xml
+++ b/java/res/layout/research_feedback_fragment_layout.xml
@@ -63,7 +63,7 @@
             android:minLines="2"
             android:scrollbars="vertical"
             android:hint="@string/research_feedback_hint"
-            android:inputType="textMultiLine">
+            android:inputType="textMultiLine|textCapSentences">
             <requestFocus />
         </EditText>
         <CheckBox
diff --git a/java/res/values-es-rUS/strings.xml b/java/res/values-es-rUS/strings.xml
index 51a6dcc..53d7598 100644
--- a/java/res/values-es-rUS/strings.xml
+++ b/java/res/values-es-rUS/strings.xml
@@ -40,7 +40,7 @@
     <string name="show_language_switch_key" msgid="5915478828318774384">"Tecla de selección de idioma"</string>
     <string name="show_language_switch_key_summary" msgid="7343403647474265713">"Mostrar cuando se habiliten varios idiomas de entrada"</string>
     <string name="sliding_key_input_preview" msgid="6604262359510068370">"Mostrar indicador p. deslizar"</string>
-    <string name="sliding_key_input_preview_summary" msgid="6340524345729093886">"Mostrar indic. visual mientras se desliza de tecl. símb. o mayús"</string>
+    <string name="sliding_key_input_preview_summary" msgid="6340524345729093886">"Mostrar indic. visual al deslizar de teclas símbolo o mayúscula"</string>
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"Retraso en rechazo de alerta de tecla"</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"Sin demora"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"Predeterminada"</string>
diff --git a/java/res/values-hr/strings.xml b/java/res/values-hr/strings.xml
index 78fba96..8432ed5 100644
--- a/java/res/values-hr/strings.xml
+++ b/java/res/values-hr/strings.xml
@@ -40,7 +40,7 @@
     <string name="show_language_switch_key" msgid="5915478828318774384">"Tipka za izmjenjivanje jezika"</string>
     <string name="show_language_switch_key_summary" msgid="7343403647474265713">"Prikaži kada je omogućen unos na više jezika"</string>
     <string name="sliding_key_input_preview" msgid="6604262359510068370">"Prikaži pokazivač klizanja"</string>
-    <string name="sliding_key_input_preview_summary" msgid="6340524345729093886">"Prikaži vizualni znak tijekom klizanja od tipki Shift ili Simbol"</string>
+    <string name="sliding_key_input_preview_summary" msgid="6340524345729093886">"Prikaži vizualni znak tijekom klizanja od tipke Shift ili tipki sa znakovima"</string>
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"Odgoda prikaza tipki"</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"Bez odgode"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"Zadano"</string>
diff --git a/java/res/values-ro/strings.xml b/java/res/values-ro/strings.xml
index bb80542..5c6ff3f 100644
--- a/java/res/values-ro/strings.xml
+++ b/java/res/values-ro/strings.xml
@@ -40,7 +40,7 @@
     <string name="show_language_switch_key" msgid="5915478828318774384">"Tastă comutare limbi"</string>
     <string name="show_language_switch_key_summary" msgid="7343403647474265713">"Afişaţi când sunt activate mai multe limbi de intrare"</string>
     <string name="sliding_key_input_preview" msgid="6604262359510068370">"Afișați indicator glisare"</string>
-    <string name="sliding_key_input_preview_summary" msgid="6340524345729093886">"Afișați sugestie vizuală la glisarea de la Shift sau Simbol"</string>
+    <string name="sliding_key_input_preview_summary" msgid="6340524345729093886">"Afișați un indicator în timpul glisării de la Shift sau tasta de simboluri"</string>
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"Înt. înch. pop-up esenţ."</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"Fără întârziere"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"Prestabilit"</string>
diff --git a/java/res/values-zh-rCN/strings.xml b/java/res/values-zh-rCN/strings.xml
index 6ee06d8..bc94938 100644
--- a/java/res/values-zh-rCN/strings.xml
+++ b/java/res/values-zh-rCN/strings.xml
@@ -39,7 +39,7 @@
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"语言切换键也可用于切换其他输入法"</string>
     <string name="show_language_switch_key" msgid="5915478828318774384">"语言切换键"</string>
     <string name="show_language_switch_key_summary" msgid="7343403647474265713">"启用了多种输入语言时显示"</string>
-    <string name="sliding_key_input_preview" msgid="6604262359510068370">"显示滑动指示器"</string>
+    <string name="sliding_key_input_preview" msgid="6604262359510068370">"显示滑动指示效果"</string>
     <string name="sliding_key_input_preview_summary" msgid="6340524345729093886">"从 Shift 键或符号键滑动时显示视觉提示"</string>
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"弹出字符隐藏延迟"</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"无延迟"</string>
diff --git a/java/res/values/research_strings.xml b/java/res/values/research_strings.xml
index f67943e..e738711 100644
--- a/java/res/values/research_strings.xml
+++ b/java/res/values/research_strings.xml
@@ -21,8 +21,20 @@
     <!-- Contents of note explaining what data is collected and how. -->
     <!-- TODO: remove translatable=false attribute once text is stable -->
     <string name="research_splash_content" translatable="false"></string>
+    <!-- Account type allowed for inclusion in user-invoked feedback logs [CHAR LIMIT=38] -->
     <string name="research_account_type" translatable="false"></string>
+    <!-- Account domain allowed for inclusion in user-invoked feedback logs [CHAR LIMIT=38] -->
     <string name="research_allowed_account_domain" translatable="false"></string>
+
+    <!-- Menu option that lets user send feedback for research purposes about the IME [CHAR LIMIT=38] -->
+    <!-- TODO: remove translatable=false attribute once text is stable -->
+    <string name="research_feedback_menu_option" translatable="false">Send feedback</string>
+    <!-- Title of dialog box that lets user send feedback for research purposes about the IME [CHAR LIMIT=38] -->
+    <!-- TODO: remove translatable=false attribute once text is stable -->
+    <string name="research_feedback_dialog_title" translatable="false">Send feedback</string>
+    <!-- Hint to user about the text entry field where they should enter research feedback [CHAR LIMIT=40] -->
+    <!-- TODO: remove translatable=false attribute once text is stable -->
+    <string name="research_feedback_hint" translatable="false">Enter your feedback here.</string>
     <!-- Message informing the user that the feedback string must not be empty [CHAR LIMIT=100] -->
     <!-- TODO: remove translatable=false attribute once text is stable -->
     <string name="research_feedback_empty_feedback_error_message" translatable="false">The feedback field must not be empty.</string>
diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml
index bab612b..ee79b45 100644
--- a/java/res/values/strings.xml
+++ b/java/res/values/strings.xml
@@ -71,7 +71,7 @@
 
     <!-- Option to enable sliding key input indicator. The user can see a rubber band-like effect during sliding key input. [CHAR LIMIT=30]-->
     <string name="sliding_key_input_preview">Show slide indicator</string>
-    <!-- Option summary to enable sliding key input indicator. The user can see a rubber band-like effect during sliding key input. [CHAR LIMIT=65]-->
+    <!-- Option summary to enable sliding key input indicator. The user can see a rubber band-like effect during sliding key input. [CHAR LIMIT=66]-->
     <string name="sliding_key_input_preview_summary">Display visual cue while sliding from Shift or Symbol keys</string>
 
     <!-- Option for the dismiss delay of the key popup [CHAR LIMIT=25] -->
@@ -266,12 +266,6 @@
     <!-- TODO: remove translatable=false attribute once text is stable -->
     <string name="research_notify_session_logging_enabled" translatable="false">Session logging enabled</string>
 
-    <!-- Menu option that lets user send feedback for research purposes about the IME [CHAR LIMIT=38] -->
-    <!-- TODO: remove translatable=false attribute once text is stable -->
-    <string name="research_feedback_menu_option" translatable="false">Send feedback</string>
-    <!-- Dialog box title that lets user send feedback for research purposes about the IME [CHAR LIMIT=38] -->
-    <!-- TODO: remove translatable=false attribute once text is stable -->
-    <string name="research_feedback_dialog_title" translatable="false">Send feedback</string>
     <!-- Text for checkbox option to include user data in feedback for research purposes [CHAR LIMIT=50] -->
     <!-- TODO: remove translatable=false attribute once text is stable -->
     <string name="research_feedback_include_history_label" translatable="false">Include session history</string>
@@ -281,9 +275,6 @@
     <!-- Text for checkbox option to include a recording in feedback for research purposes [CHAR LIMIT=50] -->
     <!-- TODO: remove translatable=false attribute once text is stable -->
     <string name="research_feedback_include_recording_label" translatable="false">Include recorded demonstration</string>
-    <!-- Hint to user about the text entry field where they should enter research feedback [CHAR LIMIT=40] -->
-    <!-- TODO: remove translatable=false attribute once text is stable -->
-    <string name="research_feedback_hint" translatable="false">Enter your feedback here.</string>
     <!-- Dialog button choice to send research feedback [CHAR LIMIT=35] -->
     <!-- TODO: remove translatable=false attribute once text is stable -->
     <string name="research_feedback_send" translatable="false">Send</string>
diff --git a/java/src/com/android/inputmethod/compat/CompatUtils.java b/java/src/com/android/inputmethod/compat/CompatUtils.java
index a82103a..5a2b6bd 100644
--- a/java/src/com/android/inputmethod/compat/CompatUtils.java
+++ b/java/src/com/android/inputmethod/compat/CompatUtils.java
@@ -16,7 +16,6 @@
 
 package com.android.inputmethod.compat;
 
-import android.content.Intent;
 import android.text.TextUtils;
 import android.util.Log;
 
@@ -26,23 +25,9 @@
 
 public final class CompatUtils {
     private static final String TAG = CompatUtils.class.getSimpleName();
-    private static final String EXTRA_INPUT_METHOD_ID = "input_method_id";
-    // TODO: Can these be constants instead of literal String constants?
-    private static final String INPUT_METHOD_SUBTYPE_SETTINGS =
-            "android.settings.INPUT_METHOD_SUBTYPE_SETTINGS";
 
-    public static Intent getInputLanguageSelectionIntent(final String inputMethodId,
-            final int flagsForSubtypeSettings) {
-        // Refer to android.provider.Settings.ACTION_INPUT_METHOD_SUBTYPE_SETTINGS
-        final String action = INPUT_METHOD_SUBTYPE_SETTINGS;
-        final Intent intent = new Intent(action);
-        if (!TextUtils.isEmpty(inputMethodId)) {
-            intent.putExtra(EXTRA_INPUT_METHOD_ID, inputMethodId);
-        }
-        if (flagsForSubtypeSettings > 0) {
-            intent.setFlags(flagsForSubtypeSettings);
-        }
-        return intent;
+    private CompatUtils() {
+        // This utility class is not publicly instantiable.
     }
 
     public static Class<?> getClass(final String className) {
diff --git a/java/src/com/android/inputmethod/latin/AdditionalSubtypeSettings.java b/java/src/com/android/inputmethod/latin/AdditionalSubtypeSettings.java
index f787722..ff5e339 100644
--- a/java/src/com/android/inputmethod/latin/AdditionalSubtypeSettings.java
+++ b/java/src/com/android/inputmethod/latin/AdditionalSubtypeSettings.java
@@ -44,8 +44,6 @@
 import android.widget.SpinnerAdapter;
 import android.widget.Toast;
 
-import com.android.inputmethod.compat.CompatUtils;
-
 import java.util.ArrayList;
 import java.util.TreeSet;
 
@@ -519,7 +517,7 @@
                 .setPositiveButton(R.string.enable, new DialogInterface.OnClickListener() {
                     @Override
                     public void onClick(DialogInterface dialog, int which) {
-                        final Intent intent = CompatUtils.getInputLanguageSelectionIntent(
+                        final Intent intent = IntentUtils.getInputLanguageSelectionIntent(
                                 mRichImm.getInputMethodIdOfThisIme(),
                                 Intent.FLAG_ACTIVITY_NEW_TASK
                                 | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
diff --git a/java/src/com/android/inputmethod/latin/IntentUtils.java b/java/src/com/android/inputmethod/latin/IntentUtils.java
new file mode 100644
index 0000000..d175af5
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/IntentUtils.java
@@ -0,0 +1,45 @@
+/*
+ * 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.content.Intent;
+import android.text.TextUtils;
+
+public final class IntentUtils {
+    private static final String EXTRA_INPUT_METHOD_ID = "input_method_id";
+    // TODO: Can these be constants instead of literal String constants?
+    private static final String INPUT_METHOD_SUBTYPE_SETTINGS =
+            "android.settings.INPUT_METHOD_SUBTYPE_SETTINGS";
+
+    private IntentUtils() {
+        // This utility class is not publicly instantiable.
+    }
+
+    public static Intent getInputLanguageSelectionIntent(final String inputMethodId,
+            final int flagsForSubtypeSettings) {
+        // Refer to android.provider.Settings.ACTION_INPUT_METHOD_SUBTYPE_SETTINGS
+        final String action = INPUT_METHOD_SUBTYPE_SETTINGS;
+        final Intent intent = new Intent(action);
+        if (!TextUtils.isEmpty(inputMethodId)) {
+            intent.putExtra(EXTRA_INPUT_METHOD_ID, inputMethodId);
+        }
+        if (flagsForSubtypeSettings > 0) {
+            intent.setFlags(flagsForSubtypeSettings);
+        }
+        return intent;
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 0821732..1c49bb0 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -62,7 +62,6 @@
 import com.android.inputmethod.accessibility.AccessibilityUtils;
 import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy;
 import com.android.inputmethod.annotations.UsedForTesting;
-import com.android.inputmethod.compat.CompatUtils;
 import com.android.inputmethod.compat.InputMethodServiceCompatUtils;
 import com.android.inputmethod.compat.SuggestionSpanUtils;
 import com.android.inputmethod.event.EventInterpreter;
@@ -2512,7 +2511,7 @@
                 di.dismiss();
                 switch (position) {
                 case 0:
-                    Intent intent = CompatUtils.getInputLanguageSelectionIntent(
+                    final Intent intent = IntentUtils.getInputLanguageSelectionIntent(
                             mRichImm.getInputMethodIdOfThisIme(),
                             Intent.FLAG_ACTIVITY_NEW_TASK
                             | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
diff --git a/java/src/com/android/inputmethod/research/FeedbackFragment.java b/java/src/com/android/inputmethod/research/FeedbackFragment.java
index 69ddf82..39f9c87 100644
--- a/java/src/com/android/inputmethod/research/FeedbackFragment.java
+++ b/java/src/com/android/inputmethod/research/FeedbackFragment.java
@@ -36,8 +36,8 @@
 public class FeedbackFragment extends Fragment implements OnClickListener {
     private static final String TAG = FeedbackFragment.class.getSimpleName();
 
-    private static final String KEY_FEEDBACK_STRING = "FeedbackString";
-    private static final String KEY_INCLUDE_ACCOUNT_NAME = "IncludeAccountName";
+    public static final String KEY_FEEDBACK_STRING = "FeedbackString";
+    public static final String KEY_INCLUDE_ACCOUNT_NAME = "IncludeAccountName";
     public static final String KEY_HAS_USER_RECORDING = "HasRecording";
 
     private EditText mEditText;
diff --git a/java/src/com/android/inputmethod/research/LogStatement.java b/java/src/com/android/inputmethod/research/LogStatement.java
index 1d83e1a..059146a 100644
--- a/java/src/com/android/inputmethod/research/LogStatement.java
+++ b/java/src/com/android/inputmethod/research/LogStatement.java
@@ -16,6 +16,18 @@
 
 package com.android.inputmethod.research;
 
+import android.content.SharedPreferences;
+import android.util.JsonWriter;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.inputmethod.CompletionInfo;
+
+import com.android.inputmethod.keyboard.Key;
+import com.android.inputmethod.latin.SuggestedWords;
+import com.android.inputmethod.latin.define.ProductionFlag;
+
+import java.io.IOException;
+
 /**
  * A template for typed information stored in the logs.
  *
@@ -24,6 +36,9 @@
  * actual values are stored separately.
  */
 class LogStatement {
+    private static final String TAG = LogStatement.class.getSimpleName();
+    private static final boolean DEBUG = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG;
+
     // Constants for particular statements
     public static final String TYPE_POINTER_TRACKER_CALL_LISTENER_ON_CODE_INPUT =
             "PointerTrackerCallListenerOnCodeInput";
@@ -36,6 +51,11 @@
     public static final String TYPE_MOTION_EVENT = "MotionEvent";
     public static final String KEY_IS_LOGGING_RELATED = "isLoggingRelated";
 
+    // Keys for internal key/value pairs
+    private static final String CURRENT_TIME_KEY = "_ct";
+    private static final String UPTIME_KEY = "_ut";
+    private static final String EVENT_TYPE_KEY = "_ty";
+
     // Name specifying the LogStatement type.
     private final String mType;
 
@@ -142,4 +162,61 @@
         }
         return false;
     }
+
+    /**
+     * Write the contents out through jsonWriter.
+     *
+     * Note that this method is not thread safe for the same jsonWriter.  Callers must ensure
+     * thread safety.
+     */
+    public boolean outputToLocked(final JsonWriter jsonWriter, final Long time,
+            final Object... values) {
+        if (DEBUG) {
+            if (mKeys.length != values.length) {
+                Log.d(TAG, "Key and Value list sizes do not match. " + mType);
+            }
+        }
+        try {
+            jsonWriter.beginObject();
+            jsonWriter.name(CURRENT_TIME_KEY).value(System.currentTimeMillis());
+            jsonWriter.name(UPTIME_KEY).value(time);
+            jsonWriter.name(EVENT_TYPE_KEY).value(mType);
+            final int length = values.length;
+            for (int i = 0; i < length; i++) {
+                jsonWriter.name(mKeys[i]);
+                final Object value = values[i];
+                if (value instanceof CharSequence) {
+                    jsonWriter.value(value.toString());
+                } else if (value instanceof Number) {
+                    jsonWriter.value((Number) value);
+                } else if (value instanceof Boolean) {
+                    jsonWriter.value((Boolean) value);
+                } else if (value instanceof CompletionInfo[]) {
+                    JsonUtils.writeJson((CompletionInfo[]) value, jsonWriter);
+                } else if (value instanceof SharedPreferences) {
+                    JsonUtils.writeJson((SharedPreferences) value, jsonWriter);
+                } else if (value instanceof Key[]) {
+                    JsonUtils.writeJson((Key[]) value, jsonWriter);
+                } else if (value instanceof SuggestedWords) {
+                    JsonUtils.writeJson((SuggestedWords) value, jsonWriter);
+                } else if (value instanceof MotionEvent) {
+                    JsonUtils.writeJson((MotionEvent) value, jsonWriter);
+                } else if (value == null) {
+                    jsonWriter.nullValue();
+                } else {
+                    if (DEBUG) {
+                        Log.w(TAG, "Unrecognized type to be logged: "
+                                + (value == null ? "<null>" : value.getClass().getName()));
+                    }
+                    jsonWriter.nullValue();
+                }
+            }
+            jsonWriter.endObject();
+        } catch (IOException e) {
+            e.printStackTrace();
+            Log.w(TAG, "Error in JsonWriter; skipping LogStatement");
+            return false;
+        }
+        return true;
+    }
 }
diff --git a/java/src/com/android/inputmethod/research/LogUnit.java b/java/src/com/android/inputmethod/research/LogUnit.java
index 2e732fc..a584a3a 100644
--- a/java/src/com/android/inputmethod/research/LogUnit.java
+++ b/java/src/com/android/inputmethod/research/LogUnit.java
@@ -17,13 +17,11 @@
 package com.android.inputmethod.research;
 
 import android.content.SharedPreferences;
+import android.os.SystemClock;
 import android.text.TextUtils;
 import android.util.JsonWriter;
 import android.util.Log;
-import android.view.MotionEvent;
-import android.view.inputmethod.CompletionInfo;
 
-import com.android.inputmethod.keyboard.Key;
 import com.android.inputmethod.latin.SuggestedWords;
 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
 import com.android.inputmethod.latin.define.ProductionFlag;
@@ -153,11 +151,10 @@
                     jsonWriter = researchLog.getValidJsonWriterLocked();
                     outputLogUnitStart(jsonWriter, canIncludePrivateData);
                 }
-                outputLogStatementToLocked(jsonWriter, mLogStatementList.get(i), mValuesList.get(i),
-                        mTimeList.get(i));
+                logStatement.outputToLocked(jsonWriter, mTimeList.get(i), mValuesList.get(i));
                 if (DEBUG) {
-                    outputLogStatementToLocked(debugJsonWriter, mLogStatementList.get(i),
-                            mValuesList.get(i), mTimeList.get(i));
+                    logStatement.outputToLocked(debugJsonWriter, mTimeList.get(i),
+                            mValuesList.get(i));
                 }
             }
             if (jsonWriter != null) {
@@ -180,97 +177,34 @@
         }
     }
 
-    private static final String CURRENT_TIME_KEY = "_ct";
-    private static final String UPTIME_KEY = "_ut";
-    private static final String EVENT_TYPE_KEY = "_ty";
     private static final String WORD_KEY = "_wo";
     private static final String CORRECTION_TYPE_KEY = "_corType";
     private static final String LOG_UNIT_BEGIN_KEY = "logUnitStart";
     private static final String LOG_UNIT_END_KEY = "logUnitEnd";
 
+    final LogStatement LOGSTATEMENT_LOG_UNIT_BEGIN_WITH_PRIVATE_DATA =
+            new LogStatement(LOG_UNIT_BEGIN_KEY, false /* isPotentiallyPrivate */,
+                    false /* isPotentiallyRevealing */, WORD_KEY, CORRECTION_TYPE_KEY);
+    final LogStatement LOGSTATEMENT_LOG_UNIT_BEGIN_WITHOUT_PRIVATE_DATA =
+            new LogStatement(LOG_UNIT_BEGIN_KEY, false /* isPotentiallyPrivate */,
+                    false /* isPotentiallyRevealing */);
     private void outputLogUnitStart(final JsonWriter jsonWriter,
             final boolean canIncludePrivateData) {
-        try {
-            jsonWriter.beginObject();
-            jsonWriter.name(CURRENT_TIME_KEY).value(System.currentTimeMillis());
-            if (canIncludePrivateData) {
-                jsonWriter.name(WORD_KEY).value(getWord());
-                jsonWriter.name(CORRECTION_TYPE_KEY).value(getCorrectionType());
-            }
-            jsonWriter.name(EVENT_TYPE_KEY).value(LOG_UNIT_BEGIN_KEY);
-            jsonWriter.endObject();
-        } catch (IOException e) {
-            e.printStackTrace();
-            Log.w(TAG, "Error in JsonWriter; cannot write LogUnitStart");
+        final LogStatement logStatement;
+        if (canIncludePrivateData) {
+            LOGSTATEMENT_LOG_UNIT_BEGIN_WITH_PRIVATE_DATA.outputToLocked(jsonWriter,
+                    SystemClock.uptimeMillis(), getWord(), getCorrectionType());
+        } else {
+            LOGSTATEMENT_LOG_UNIT_BEGIN_WITHOUT_PRIVATE_DATA.outputToLocked(jsonWriter,
+                    SystemClock.uptimeMillis());
         }
     }
 
+    final LogStatement LOGSTATEMENT_LOG_UNIT_END =
+            new LogStatement(LOG_UNIT_END_KEY, false /* isPotentiallyPrivate */,
+                    false /* isPotentiallyRevealing */);
     private void outputLogUnitStop(final JsonWriter jsonWriter) {
-        try {
-            jsonWriter.beginObject();
-            jsonWriter.name(CURRENT_TIME_KEY).value(System.currentTimeMillis());
-            jsonWriter.name(EVENT_TYPE_KEY).value(LOG_UNIT_END_KEY);
-            jsonWriter.endObject();
-        } catch (IOException e) {
-            e.printStackTrace();
-            Log.w(TAG, "Error in JsonWriter; cannot write LogUnitStop");
-        }
-    }
-
-    /**
-     * Write the logStatement and its contents out through jsonWriter.
-     *
-     * Note that this method is not thread safe for the same jsonWriter.  Callers must ensure
-     * thread safety.
-     */
-    private boolean outputLogStatementToLocked(final JsonWriter jsonWriter,
-            final LogStatement logStatement, final Object[] values, final Long time) {
-        if (DEBUG) {
-            if (logStatement.getKeys().length != values.length) {
-                Log.d(TAG, "Key and Value list sizes do not match. " + logStatement.getType());
-            }
-        }
-        try {
-            jsonWriter.beginObject();
-            jsonWriter.name(CURRENT_TIME_KEY).value(System.currentTimeMillis());
-            jsonWriter.name(UPTIME_KEY).value(time);
-            jsonWriter.name(EVENT_TYPE_KEY).value(logStatement.getType());
-            final String[] keys = logStatement.getKeys();
-            final int length = values.length;
-            for (int i = 0; i < length; i++) {
-                jsonWriter.name(keys[i]);
-                final Object value = values[i];
-                if (value instanceof CharSequence) {
-                    jsonWriter.value(value.toString());
-                } else if (value instanceof Number) {
-                    jsonWriter.value((Number) value);
-                } else if (value instanceof Boolean) {
-                    jsonWriter.value((Boolean) value);
-                } else if (value instanceof CompletionInfo[]) {
-                    JsonUtils.writeJson((CompletionInfo[]) value, jsonWriter);
-                } else if (value instanceof SharedPreferences) {
-                    JsonUtils.writeJson((SharedPreferences) value, jsonWriter);
-                } else if (value instanceof Key[]) {
-                    JsonUtils.writeJson((Key[]) value, jsonWriter);
-                } else if (value instanceof SuggestedWords) {
-                    JsonUtils.writeJson((SuggestedWords) value, jsonWriter);
-                } else if (value instanceof MotionEvent) {
-                    JsonUtils.writeJson((MotionEvent) value, jsonWriter);
-                } else if (value == null) {
-                    jsonWriter.nullValue();
-                } else {
-                    Log.w(TAG, "Unrecognized type to be logged: "
-                            + (value == null ? "<null>" : value.getClass().getName()));
-                    jsonWriter.nullValue();
-                }
-            }
-            jsonWriter.endObject();
-        } catch (IOException e) {
-            e.printStackTrace();
-            Log.w(TAG, "Error in JsonWriter; skipping LogStatement");
-            return false;
-        }
-        return true;
+        LOGSTATEMENT_LOG_UNIT_END.outputToLocked(jsonWriter, SystemClock.uptimeMillis());
     }
 
     /**
diff --git a/java/src/com/android/inputmethod/research/MotionEventReader.java b/java/src/com/android/inputmethod/research/MotionEventReader.java
index 26a1d7f..e59adfa 100644
--- a/java/src/com/android/inputmethod/research/MotionEventReader.java
+++ b/java/src/com/android/inputmethod/research/MotionEventReader.java
@@ -19,6 +19,8 @@
 import android.util.JsonReader;
 import android.util.Log;
 import android.view.MotionEvent;
+import android.view.MotionEvent.PointerCoords;
+import android.view.MotionEvent.PointerProperties;
 
 import com.android.inputmethod.latin.define.ProductionFlag;
 
@@ -33,6 +35,14 @@
 public class MotionEventReader {
     private static final String TAG = MotionEventReader.class.getSimpleName();
     private static final boolean DEBUG = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG;
+    // Assumes that MotionEvent.ACTION_MASK does not have all bits set.`
+    private static final int UNINITIALIZED_ACTION = ~MotionEvent.ACTION_MASK;
+    // No legitimate int is negative
+    private static final int UNINITIALIZED_INT = -1;
+    // No legitimate long is negative
+    private static final long UNINITIALIZED_LONG = -1L;
+    // No legitimate float is negative
+    private static final float UNINITIALIZED_FLOAT = -1.0f;
 
     public ReplayData readMotionEventData(final File file) {
         final ReplayData replayData = new ReplayData();
@@ -55,19 +65,82 @@
 
     static class ReplayData {
         final ArrayList<Integer> mActions = new ArrayList<Integer>();
-        final ArrayList<Integer> mXCoords = new ArrayList<Integer>();
-        final ArrayList<Integer> mYCoords = new ArrayList<Integer>();
+        final ArrayList<PointerProperties[]> mPointerPropertiesArrays
+                = new ArrayList<PointerProperties[]>();
+        final ArrayList<PointerCoords[]> mPointerCoordsArrays = new ArrayList<PointerCoords[]>();
         final ArrayList<Long> mTimes = new ArrayList<Long>();
     }
 
-    private void readLogStatement(final JsonReader jsonReader, final ReplayData replayData)
-            throws IOException {
+    /**
+     * Read motion data from a logStatement and store it in {@code replayData}.
+     *
+     * Two kinds of logStatements can be read.  In the first variant, the MotionEvent data is
+     * represented as attributes at the top level like so:
+     *
+     * <pre>
+     * {
+     *   "_ct": 1359590400000,
+     *   "_ut": 4381933,
+     *   "_ty": "MotionEvent",
+     *   "action": "UP",
+     *   "isLoggingRelated": false,
+     *   "x": 100,
+     *   "y": 200
+     * }
+     * </pre>
+     *
+     * In the second variant, there is a separate attribute for the MotionEvent that includes
+     * historical data if present:
+     *
+     * <pre>
+     * {
+     *   "_ct": 135959040000,
+     *   "_ut": 4382702,
+     *   "_ty": "MotionEvent",
+     *   "action": "MOVE",
+     *   "isLoggingRelated": false,
+     *   "motionEvent": {
+     *     "pointerIds": [
+     *       0
+     *     ],
+     *     "xyt": [
+     *       {
+     *         "t": 4382551,
+     *         "d": [
+     *           {
+     *             "x": 141.25,
+     *             "y": 151.8485107421875,
+     *             "toma": 101.82337188720703,
+     *             "tomi": 101.82337188720703,
+     *             "o": 0.0
+     *           }
+     *         ]
+     *       },
+     *       {
+     *         "t": 4382559,
+     *         "d": [
+     *           {
+     *             "x": 140.7266082763672,
+     *             "y": 151.8485107421875,
+     *             "toma": 101.82337188720703,
+     *             "tomi": 101.82337188720703,
+     *             "o": 0.0
+     *           }
+     *         ]
+     *       }
+     *     ]
+     *   }
+     * },
+     * </pre>
+     */
+    /* package for test */ void readLogStatement(final JsonReader jsonReader,
+            final ReplayData replayData) throws IOException {
         String logStatementType = null;
-        Integer actionType = null;
-        Integer x = null;
-        Integer y = null;
-        Long time = null;
-        boolean loggingRelated = false;
+        int actionType = UNINITIALIZED_ACTION;
+        int x = UNINITIALIZED_INT;
+        int y = UNINITIALIZED_INT;
+        long time = UNINITIALIZED_LONG;
+        boolean isLoggingRelated = false;
 
         jsonReader.beginObject();
         while (jsonReader.hasNext()) {
@@ -90,7 +163,18 @@
                     actionType = MotionEvent.ACTION_MOVE;
                 }
             } else if (key.equals("loggingRelated")) {
-                loggingRelated = jsonReader.nextBoolean();
+                isLoggingRelated = jsonReader.nextBoolean();
+            } else if (logStatementType != null && logStatementType.equals("MotionEvent")
+                    && key.equals("motionEvent")) {
+                if (actionType == UNINITIALIZED_ACTION) {
+                    Log.e(TAG, "no actionType assigned in MotionEvent json");
+                }
+                // Second variant of LogStatement.
+                if (isLoggingRelated) {
+                    jsonReader.skipValue();
+                } else {
+                    readEmbeddedMotionEvent(jsonReader, replayData, actionType);
+                }
             } else {
                 if (DEBUG) {
                     Log.w(TAG, "Unknown JSON key in LogStatement: " + key);
@@ -100,14 +184,149 @@
         }
         jsonReader.endObject();
 
-        if (logStatementType != null && time != null && x != null && y != null && actionType != null
-                && logStatementType.equals("MotionEvent")
-                && !loggingRelated) {
-            replayData.mActions.add(actionType);
-            replayData.mXCoords.add(x);
-            replayData.mYCoords.add(y);
-            replayData.mTimes.add(time);
+        if (logStatementType != null && time != UNINITIALIZED_LONG && x != UNINITIALIZED_INT
+                && y != UNINITIALIZED_INT && actionType != UNINITIALIZED_ACTION
+                && logStatementType.equals("MotionEvent") && !isLoggingRelated) {
+            // First variant of LogStatement.
+            final PointerProperties pointerProperties = new PointerProperties();
+            pointerProperties.id = 0;
+            pointerProperties.toolType = MotionEvent.TOOL_TYPE_UNKNOWN;
+            final PointerProperties[] pointerPropertiesArray = {
+                pointerProperties
+            };
+            final PointerCoords pointerCoords = new PointerCoords();
+            pointerCoords.x = x;
+            pointerCoords.y = y;
+            pointerCoords.pressure = 1.0f;
+            pointerCoords.size = 1.0f;
+            final PointerCoords[] pointerCoordsArray = {
+                pointerCoords
+            };
+            addMotionEventData(replayData, actionType, time, pointerPropertiesArray,
+                    pointerCoordsArray);
         }
     }
 
+    private void readEmbeddedMotionEvent(final JsonReader jsonReader, final ReplayData replayData,
+            final int actionType) throws IOException {
+        jsonReader.beginObject();
+        PointerProperties[] pointerPropertiesArray = null;
+        while (jsonReader.hasNext()) {  // pointerIds/xyt
+            final String name = jsonReader.nextName();
+            if (name.equals("pointerIds")) {
+                pointerPropertiesArray = readPointerProperties(jsonReader);
+            } else if (name.equals("xyt")) {
+                readPointerData(jsonReader, replayData, actionType, pointerPropertiesArray);
+            }
+        }
+        jsonReader.endObject();
+    }
+
+    private PointerProperties[] readPointerProperties(final JsonReader jsonReader)
+            throws IOException {
+        final ArrayList<PointerProperties> pointerPropertiesArrayList =
+                new ArrayList<PointerProperties>();
+        jsonReader.beginArray();
+        while (jsonReader.hasNext()) {
+            final PointerProperties pointerProperties = new PointerProperties();
+            pointerProperties.id = jsonReader.nextInt();
+            pointerProperties.toolType = MotionEvent.TOOL_TYPE_UNKNOWN;
+            pointerPropertiesArrayList.add(pointerProperties);
+        }
+        jsonReader.endArray();
+        return pointerPropertiesArrayList.toArray(
+                new PointerProperties[pointerPropertiesArrayList.size()]);
+    }
+
+    private void readPointerData(final JsonReader jsonReader, final ReplayData replayData,
+            final int actionType, final PointerProperties[] pointerPropertiesArray)
+            throws IOException {
+        if (pointerPropertiesArray == null) {
+            Log.e(TAG, "PointerIDs must be given before xyt data in json for MotionEvent");
+            jsonReader.skipValue();
+            return;
+        }
+        long time = UNINITIALIZED_LONG;
+        jsonReader.beginArray();
+        while (jsonReader.hasNext()) {  // Array of historical data
+            jsonReader.beginObject();
+            final ArrayList<PointerCoords> pointerCoordsArrayList = new ArrayList<PointerCoords>();
+            while (jsonReader.hasNext()) {  // Time/data object
+                final String name = jsonReader.nextName();
+                if (name.equals("t")) {
+                    time = jsonReader.nextLong();
+                } else if (name.equals("d")) {
+                    jsonReader.beginArray();
+                    while (jsonReader.hasNext()) {  // array of data per pointer
+                        final PointerCoords pointerCoords = readPointerCoords(jsonReader);
+                        if (pointerCoords != null) {
+                            pointerCoordsArrayList.add(pointerCoords);
+                        }
+                    }
+                    jsonReader.endArray();
+                } else {
+                    jsonReader.skipValue();
+                }
+            }
+            jsonReader.endObject();
+            // Data was recorded as historical events, but must be split apart into
+            // separate MotionEvents for replaying
+            if (time != UNINITIALIZED_LONG) {
+                addMotionEventData(replayData, actionType, time, pointerPropertiesArray,
+                        pointerCoordsArrayList.toArray(
+                                new PointerCoords[pointerCoordsArrayList.size()]));
+            } else {
+                Log.e(TAG, "Time not assigned in json for MotionEvent");
+            }
+        }
+        jsonReader.endArray();
+    }
+
+    private PointerCoords readPointerCoords(final JsonReader jsonReader) throws IOException {
+        jsonReader.beginObject();
+        float x = UNINITIALIZED_FLOAT;
+        float y = UNINITIALIZED_FLOAT;
+        while (jsonReader.hasNext()) {  // x,y
+            final String name = jsonReader.nextName();
+            if (name.equals("x")) {
+                x = (float) jsonReader.nextDouble();
+            } else if (name.equals("y")) {
+                y = (float) jsonReader.nextDouble();
+            } else {
+                jsonReader.skipValue();
+            }
+        }
+        jsonReader.endObject();
+
+        if (Float.compare(x, UNINITIALIZED_FLOAT) == 0
+                || Float.compare(y, UNINITIALIZED_FLOAT) == 0) {
+            Log.w(TAG, "missing x or y value in MotionEvent json");
+            return null;
+        }
+        final PointerCoords pointerCoords = new PointerCoords();
+        pointerCoords.x = x;
+        pointerCoords.y = y;
+        pointerCoords.pressure = 1.0f;
+        pointerCoords.size = 1.0f;
+        return pointerCoords;
+    }
+
+    /**
+     * Tests that {@code x} is uninitialized.
+     *
+     * Assumes that {@code x} will never be given a valid value less than 0, and that
+     * UNINITIALIZED_FLOAT is less than 0.0f.
+     */
+    private boolean isUninitializedFloat(final float x) {
+        return x < 0.0f;
+    }
+
+    private void addMotionEventData(final ReplayData replayData, final int actionType,
+            final long time, final PointerProperties[] pointerProperties,
+            final PointerCoords[] pointerCoords) {
+        replayData.mActions.add(actionType);
+        replayData.mTimes.add(time);
+        replayData.mPointerPropertiesArrays.add(pointerProperties);
+        replayData.mPointerCoordsArrays.add(pointerCoords);
+    }
 }
diff --git a/java/src/com/android/inputmethod/research/Replayer.java b/java/src/com/android/inputmethod/research/Replayer.java
index 611abb2..a9b7a9d 100644
--- a/java/src/com/android/inputmethod/research/Replayer.java
+++ b/java/src/com/android/inputmethod/research/Replayer.java
@@ -22,6 +22,8 @@
 import android.os.SystemClock;
 import android.util.Log;
 import android.view.MotionEvent;
+import android.view.MotionEvent.PointerCoords;
+import android.view.MotionEvent.PointerProperties;
 
 import com.android.inputmethod.keyboard.KeyboardSwitcher;
 import com.android.inputmethod.keyboard.MainKeyboardView;
@@ -62,7 +64,6 @@
         if (mIsReplaying) {
             return;
         }
-
         mIsReplaying = true;
         final int numActions = replayData.mActions.size();
         if (DEBUG) {
@@ -95,25 +96,36 @@
                 case MSG_MOTION_EVENT:
                     final int index = msg.arg1;
                     final int action = replayData.mActions.get(index);
-                    final int x = replayData.mXCoords.get(index);
-                    final int y = replayData.mYCoords.get(index);
+                    final PointerProperties[] pointerPropertiesArray =
+                            replayData.mPointerPropertiesArrays.get(index);
+                    final PointerCoords[] pointerCoordsArray =
+                            replayData.mPointerCoordsArrays.get(index);
                     final long origTime = replayData.mTimes.get(index);
                     if (action == MotionEvent.ACTION_DOWN) {
                         mOrigDownTime = origTime;
                     }
 
                     final MotionEvent me = MotionEvent.obtain(mOrigDownTime + timeAdjustment,
-                            origTime + timeAdjustment, action, x, y, 0);
+                            origTime + timeAdjustment, action,
+                            pointerPropertiesArray.length, pointerPropertiesArray,
+                            pointerCoordsArray, 0, 0, 1.0f, 1.0f, 0, 0, 0, 0);
                     mainKeyboardView.processMotionEvent(me);
                     me.recycle();
                     break;
                 case MSG_DONE:
                     mIsReplaying = false;
+                    ResearchLogger.getInstance().requestIndicatorRedraw();
                     break;
                 }
             }
         };
 
+        handler.post(new Runnable() {
+            @Override
+            public void run() {
+                ResearchLogger.getInstance().requestIndicatorRedraw();
+            }
+        });
         for (int i = 0; i < numActions; i++) {
             final Message msg = Message.obtain(handler, MSG_MOTION_EVENT, i, 0);
             final long msgTime = replayData.mTimes.get(i) + timeAdjustment;
@@ -130,4 +142,8 @@
             handler.postAtTime(callback, presentDoneTime + 1);
         }
     }
+
+    public boolean isReplaying() {
+        return mIsReplaying;
+    }
 }
diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java
index da41001..364ab2d 100644
--- a/java/src/com/android/inputmethod/research/ResearchLogger.java
+++ b/java/src/com/android/inputmethod/research/ResearchLogger.java
@@ -115,6 +115,12 @@
     // private info.
     private static final boolean LOG_FULL_TEXTVIEW_CONTENTS = false
             && ProductionFlag.IS_EXPERIMENTAL_DEBUG;
+    // Whether the feedback dialog preserves the editable text across invocations.  Should be false
+    // for normal research builds so users do not have to delete the same feedback string they
+    // entered earlier.  Should be true for builds internal to a development team so when the text
+    // field holds a channel name, the developer does not have to re-enter it when using the
+    // feedback mechanism to generate multiple tests.
+    private static final boolean FEEDBACK_DIALOG_SHOULD_PRESERVE_TEXT_FIELD = false;
     public static final boolean DEFAULT_USABILITY_STUDY_MODE = false;
     /* package */ static boolean sIsLogging = false;
     private static final int OUTPUT_FORMAT_VERSION = 5;
@@ -140,6 +146,7 @@
     private static final String WHITESPACE_SEPARATORS = " \t\n\r";
     private static final int MAX_INPUTVIEW_LENGTH_TO_CAPTURE = 8192; // must be >=1
     private static final String PREF_RESEARCH_LOGGER_UUID_STRING = "pref_research_logger_uuid";
+    private static final String PREF_RESEARCH_SAVED_CHANNEL = "pref_research_saved_channel";
 
     private static final ResearchLogger sInstance = new ResearchLogger();
     private static String sAccountType = null;
@@ -591,12 +598,20 @@
         mFeedbackLogBuffer = null;
         mFeedbackLog = null;
 
-        Intent intent = new Intent();
+        final Intent intent = new Intent();
         intent.setClass(mLatinIME, FeedbackActivity.class);
-        if (mFeedbackDialogBundle != null) {
-            Log.d(TAG, "putting extra in feedbackdialogbundle");
-            intent.putExtras(mFeedbackDialogBundle);
+        if (mFeedbackDialogBundle == null) {
+            // Restore feedback field with channel name
+            final Bundle bundle = new Bundle();
+            bundle.putBoolean(FeedbackFragment.KEY_INCLUDE_ACCOUNT_NAME, true);
+            bundle.putBoolean(FeedbackFragment.KEY_HAS_USER_RECORDING, false);
+            if (FEEDBACK_DIALOG_SHOULD_PRESERVE_TEXT_FIELD) {
+                final String savedChannelName = mPrefs.getString(PREF_RESEARCH_SAVED_CHANNEL, "");
+                bundle.putString(FeedbackFragment.KEY_FEEDBACK_STRING, savedChannelName);
+            }
+            mFeedbackDialogBundle = bundle;
         }
+        intent.putExtras(mFeedbackDialogBundle);
         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
         latinIME.startActivity(intent);
     }
@@ -787,6 +802,18 @@
                 }
             }, 1000);
         }
+
+        if (FEEDBACK_DIALOG_SHOULD_PRESERVE_TEXT_FIELD) {
+            // Use feedback string as a channel name to label feedback strings.  Here we record the
+            // string for prepopulating the field next time.
+            final String channelName = feedbackContents;
+            if (mPrefs == null) {
+                return;
+            }
+            final Editor e = mPrefs.edit();
+            e.putString(PREF_RESEARCH_SAVED_CHANNEL, channelName);
+            e.apply();
+        }
     }
 
     public void uploadNow() {
@@ -819,7 +846,8 @@
     }
 
     private boolean isAllowedToLog() {
-        return !mIsPasswordView && !mIsLoggingSuspended && sIsLogging && !mInFeedbackDialog;
+        return !mIsPasswordView && !mIsLoggingSuspended && sIsLogging && !mInFeedbackDialog
+                && !isReplaying();
     }
 
     public void requestIndicatorRedraw() {
@@ -832,15 +860,30 @@
         mMainKeyboardView.invalidateAllKeys();
     }
 
+    private boolean isReplaying() {
+        return mReplayer.isReplaying();
+    }
+
+    private int getIndicatorColor() {
+        if (isMakingUserRecording()) {
+            return Color.YELLOW;
+        }
+        if (isReplaying()) {
+            return Color.GREEN;
+        }
+        return Color.RED;
+    }
+
     public void paintIndicator(KeyboardView view, Paint paint, Canvas canvas, int width,
             int height) {
         // TODO: Reimplement using a keyboard background image specific to the ResearchLogger
         // and remove this method.
         // The check for MainKeyboardView ensures that the indicator only decorates the main
         // keyboard, not every keyboard.
-        if (IS_SHOWING_INDICATOR && isAllowedToLog() && view instanceof MainKeyboardView) {
+        if (IS_SHOWING_INDICATOR && (isAllowedToLog() || isReplaying())
+                && view instanceof MainKeyboardView) {
             final int savedColor = paint.getColor();
-            paint.setColor(isMakingUserRecording() ? Color.YELLOW : Color.RED);
+            paint.setColor(getIndicatorColor());
             final Style savedStyle = paint.getStyle();
             paint.setStyle(Style.STROKE);
             final float savedStrokeWidth = paint.getStrokeWidth();
@@ -960,15 +1003,23 @@
         }
     }
 
+    /**
+     * Publish all the logUnits in the logBuffer, without doing any privacy filtering.
+     */
     /* package for test */ void publishLogBuffer(final LogBuffer logBuffer,
-            final ResearchLog researchLog, final boolean isIncludingPrivateData) {
-        publishLogUnits(logBuffer.getLogUnits(), researchLog, isIncludingPrivateData);
+            final ResearchLog researchLog, final boolean canIncludePrivateData) {
+        publishLogUnits(logBuffer.getLogUnits(), researchLog, canIncludePrivateData);
     }
 
     private static final LogStatement LOGSTATEMENT_LOG_SEGMENT_OPENING =
             new LogStatement("logSegmentStart", false, false, "isIncludingPrivateData");
     private static final LogStatement LOGSTATEMENT_LOG_SEGMENT_CLOSING =
             new LogStatement("logSegmentEnd", false, false);
+    /**
+     * Publish all LogUnits in a list.
+     *
+     * Any privacy checks should be performed before calling this method.
+     */
     /* package for test */ void publishLogUnits(final List<LogUnit> logUnits,
             final ResearchLog researchLog, final boolean canIncludePrivateData) {
         final LogUnit openingLogUnit = new LogUnit();
@@ -1349,7 +1400,7 @@
             final int index, final String suggestion, final boolean isBatchMode) {
         final ResearchLogger researchLogger = getInstance();
         if (!replacedWord.equals(suggestion.toString())) {
-            // The user choose something other than what was already there.
+            // The user chose something other than what was already there.
             researchLogger.setCurrentLogUnitContainsCorrection();
             researchLogger.setCurrentLogUnitCorrectionType(LogUnit.CORRECTIONTYPE_TYPO);
         }
diff --git a/tests/Android.mk b/tests/Android.mk
index 6634070..5baebbd 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -19,8 +19,6 @@
 LOCAL_MODULE_TAGS := tests
 LOCAL_CERTIFICATE := shared
 
-LOCAL_JAVA_LIBRARIES := android.test.runner
-
 # Do not compress dictionary files to mmap dict data runtime
 LOCAL_AAPT_FLAGS += -0 .dict
 # Do not compress test data file
@@ -33,4 +31,6 @@
 
 LOCAL_INSTRUMENTATION_FOR := LatinIME
 
+LOCAL_SDK_VERSION := current
+
 include $(BUILD_PACKAGE)
diff --git a/tests/src/com/android/inputmethod/keyboard/MoreKeysKeyboardBuilderFixedOrderTests.java b/tests/src/com/android/inputmethod/keyboard/MoreKeysKeyboardBuilderFixedOrderTests.java
index 2a244a7..49ef3a2 100644
--- a/tests/src/com/android/inputmethod/keyboard/MoreKeysKeyboardBuilderFixedOrderTests.java
+++ b/tests/src/com/android/inputmethod/keyboard/MoreKeysKeyboardBuilderFixedOrderTests.java
@@ -17,9 +17,11 @@
 package com.android.inputmethod.keyboard;
 
 import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
 
 import com.android.inputmethod.keyboard.MoreKeysKeyboard.MoreKeysKeyboardParams;
 
+@SmallTest
 public class MoreKeysKeyboardBuilderFixedOrderTests extends AndroidTestCase {
     private static final int WIDTH = 10;
     private static final int HEIGHT = 10;
diff --git a/tests/src/com/android/inputmethod/keyboard/MoreKeysKeyboardBuilderTests.java b/tests/src/com/android/inputmethod/keyboard/MoreKeysKeyboardBuilderTests.java
index e6c76db..93883dc 100644
--- a/tests/src/com/android/inputmethod/keyboard/MoreKeysKeyboardBuilderTests.java
+++ b/tests/src/com/android/inputmethod/keyboard/MoreKeysKeyboardBuilderTests.java
@@ -17,9 +17,11 @@
 package com.android.inputmethod.keyboard;
 
 import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
 
 import com.android.inputmethod.keyboard.MoreKeysKeyboard.MoreKeysKeyboardParams;
 
+@SmallTest
 public class MoreKeysKeyboardBuilderTests extends AndroidTestCase {
     private static final int WIDTH = 10;
     private static final int HEIGHT = 10;
diff --git a/tests/src/com/android/inputmethod/keyboard/SpacebarTextTests.java b/tests/src/com/android/inputmethod/keyboard/SpacebarTextTests.java
index 05b1376..d0426eb 100644
--- a/tests/src/com/android/inputmethod/keyboard/SpacebarTextTests.java
+++ b/tests/src/com/android/inputmethod/keyboard/SpacebarTextTests.java
@@ -19,6 +19,7 @@
 import android.content.Context;
 import android.content.res.Resources;
 import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
 import android.view.inputmethod.InputMethodSubtype;
 
 import com.android.inputmethod.latin.AdditionalSubtype;
@@ -31,6 +32,7 @@
 import java.util.ArrayList;
 import java.util.Locale;
 
+@SmallTest
 public class SpacebarTextTests extends AndroidTestCase {
     // Locale to subtypes list.
     private final ArrayList<InputMethodSubtype> mSubtypesList = CollectionUtils.newArrayList();
diff --git a/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserCsvTests.java b/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserCsvTests.java
index 1346c00..cee62ca 100644
--- a/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserCsvTests.java
+++ b/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserCsvTests.java
@@ -18,6 +18,7 @@
 
 import android.app.Instrumentation;
 import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
 
 import com.android.inputmethod.latin.CollectionUtils;
 
@@ -26,6 +27,7 @@
 import java.util.Arrays;
 import java.util.Locale;
 
+@SmallTest
 public class KeySpecParserCsvTests extends InstrumentationTestCase {
     private final KeyboardTextsSet mTextsSet = new KeyboardTextsSet();
 
diff --git a/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserTests.java b/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserTests.java
index 5bba961..a5b61b3 100644
--- a/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserTests.java
+++ b/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserTests.java
@@ -21,12 +21,14 @@
 import static com.android.inputmethod.latin.Constants.CODE_UNSPECIFIED;
 
 import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
 
 import com.android.inputmethod.latin.Constants;
 
 import java.util.Arrays;
 import java.util.Locale;
 
+@SmallTest
 public class KeySpecParserTests extends AndroidTestCase {
     private final KeyboardCodesSet mCodesSet = new KeyboardCodesSet();
     private final KeyboardTextsSet mTextsSet = new KeyboardTextsSet();
diff --git a/tests/src/com/android/inputmethod/keyboard/internal/KeyboardStateMultiTouchTests.java b/tests/src/com/android/inputmethod/keyboard/internal/KeyboardStateMultiTouchTests.java
index 053bcb5..5fbe6d4 100644
--- a/tests/src/com/android/inputmethod/keyboard/internal/KeyboardStateMultiTouchTests.java
+++ b/tests/src/com/android/inputmethod/keyboard/internal/KeyboardStateMultiTouchTests.java
@@ -16,6 +16,9 @@
 
 package com.android.inputmethod.keyboard.internal;
 
+import android.test.suitebuilder.annotation.SmallTest;
+
+@SmallTest
 public class KeyboardStateMultiTouchTests extends KeyboardStateTestsBase {
     // Chording input in alphabet.
     public void testChordingAlphabet() {
diff --git a/tests/src/com/android/inputmethod/keyboard/internal/KeyboardStateSingleTouchTests.java b/tests/src/com/android/inputmethod/keyboard/internal/KeyboardStateSingleTouchTests.java
index 74ff879..637d475 100644
--- a/tests/src/com/android/inputmethod/keyboard/internal/KeyboardStateSingleTouchTests.java
+++ b/tests/src/com/android/inputmethod/keyboard/internal/KeyboardStateSingleTouchTests.java
@@ -16,6 +16,9 @@
 
 package com.android.inputmethod.keyboard.internal;
 
+import android.test.suitebuilder.annotation.SmallTest;
+
+@SmallTest
 public class KeyboardStateSingleTouchTests extends KeyboardStateTestsBase {
     // Shift key in alphabet.
     public void testShiftAlphabet() {
diff --git a/tests/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueueTests.java b/tests/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueueTests.java
index 2c3e3a5..e7068af 100644
--- a/tests/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueueTests.java
+++ b/tests/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueueTests.java
@@ -17,7 +17,9 @@
 package com.android.inputmethod.keyboard.internal;
 
 import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
 
+@SmallTest
 public class PointerTrackerQueueTests extends AndroidTestCase {
     public static class Element implements PointerTrackerQueue.Element {
         public static int sPhantomUpCount;
diff --git a/tests/src/com/android/inputmethod/latin/BlueUnderlineTests.java b/tests/src/com/android/inputmethod/latin/BlueUnderlineTests.java
index 6b4d52d..594b5d6 100644
--- a/tests/src/com/android/inputmethod/latin/BlueUnderlineTests.java
+++ b/tests/src/com/android/inputmethod/latin/BlueUnderlineTests.java
@@ -16,9 +16,11 @@
 
 package com.android.inputmethod.latin;
 
+import android.test.suitebuilder.annotation.LargeTest;
 import android.text.style.SuggestionSpan;
 import android.text.style.UnderlineSpan;
 
+@LargeTest
 public class BlueUnderlineTests extends InputTestsBase {
 
     public void testBlueUnderline() {
diff --git a/tests/src/com/android/inputmethod/latin/EditDistanceTests.java b/tests/src/com/android/inputmethod/latin/EditDistanceTests.java
index c053a49..0b7fcbb 100644
--- a/tests/src/com/android/inputmethod/latin/EditDistanceTests.java
+++ b/tests/src/com/android/inputmethod/latin/EditDistanceTests.java
@@ -17,7 +17,9 @@
 package com.android.inputmethod.latin;
 
 import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
 
+@SmallTest
 public class EditDistanceTests extends AndroidTestCase {
     @Override
     protected void setUp() throws Exception {
diff --git a/tests/src/com/android/inputmethod/latin/ForgettingCurveTests.java b/tests/src/com/android/inputmethod/latin/ForgettingCurveTests.java
index a2e71c1..3259312 100644
--- a/tests/src/com/android/inputmethod/latin/ForgettingCurveTests.java
+++ b/tests/src/com/android/inputmethod/latin/ForgettingCurveTests.java
@@ -17,7 +17,9 @@
 package com.android.inputmethod.latin;
 
 import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
 
+@SmallTest
 public class ForgettingCurveTests extends AndroidTestCase {
     public void testFcToFreq() {
         for (int i = 0; i < Byte.MAX_VALUE; ++i) {
diff --git a/tests/src/com/android/inputmethod/latin/FusionDictionaryTests.java b/tests/src/com/android/inputmethod/latin/FusionDictionaryTests.java
index 123959b..fd58476 100644
--- a/tests/src/com/android/inputmethod/latin/FusionDictionaryTests.java
+++ b/tests/src/com/android/inputmethod/latin/FusionDictionaryTests.java
@@ -17,6 +17,7 @@
 package com.android.inputmethod.latin;
 
 import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
 
 import com.android.inputmethod.latin.makedict.FusionDictionary;
 import com.android.inputmethod.latin.makedict.FusionDictionary.Node;
@@ -26,6 +27,7 @@
 /**
  * Unit test for FusionDictionary
  */
+@SmallTest
 public class FusionDictionaryTests extends AndroidTestCase {
     public void testFindWordInTree() {
         FusionDictionary dict = new FusionDictionary(new Node(),
diff --git a/tests/src/com/android/inputmethod/latin/InputLogicTests.java b/tests/src/com/android/inputmethod/latin/InputLogicTests.java
index 6412a9d..d7b2407 100644
--- a/tests/src/com/android/inputmethod/latin/InputLogicTests.java
+++ b/tests/src/com/android/inputmethod/latin/InputLogicTests.java
@@ -16,6 +16,9 @@
 
 package com.android.inputmethod.latin;
 
+import android.test.suitebuilder.annotation.LargeTest;
+
+@LargeTest
 public class InputLogicTests extends InputTestsBase {
 
     public void testTypeWord() {
diff --git a/tests/src/com/android/inputmethod/latin/InputLogicTestsNonEnglish.java b/tests/src/com/android/inputmethod/latin/InputLogicTestsNonEnglish.java
index 42823f5..e012f79 100644
--- a/tests/src/com/android/inputmethod/latin/InputLogicTestsNonEnglish.java
+++ b/tests/src/com/android/inputmethod/latin/InputLogicTestsNonEnglish.java
@@ -16,8 +16,11 @@
 
 package com.android.inputmethod.latin;
 
+import android.test.suitebuilder.annotation.LargeTest;
+
 import com.android.inputmethod.latin.suggestions.SuggestionStripView;
 
+@LargeTest
 public class InputLogicTestsNonEnglish extends InputTestsBase {
     final String NEXT_WORD_PREDICTION_OPTION = "next_word_prediction";
 
diff --git a/tests/src/com/android/inputmethod/latin/InputPointersTests.java b/tests/src/com/android/inputmethod/latin/InputPointersTests.java
index cc55076..44526a3 100644
--- a/tests/src/com/android/inputmethod/latin/InputPointersTests.java
+++ b/tests/src/com/android/inputmethod/latin/InputPointersTests.java
@@ -17,9 +17,11 @@
 package com.android.inputmethod.latin;
 
 import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
 
 import java.util.Arrays;
 
+@SmallTest
 public class InputPointersTests extends AndroidTestCase {
     private static final int DEFAULT_CAPACITY = 48;
 
diff --git a/tests/src/com/android/inputmethod/latin/InputTestsBase.java b/tests/src/com/android/inputmethod/latin/InputTestsBase.java
index 10d415b..c440587 100644
--- a/tests/src/com/android/inputmethod/latin/InputTestsBase.java
+++ b/tests/src/com/android/inputmethod/latin/InputTestsBase.java
@@ -31,16 +31,12 @@
 import android.view.ViewGroup;
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.InputConnection;
-import android.view.inputmethod.InputMethodInfo;
-import android.view.inputmethod.InputMethodManager;
-import android.view.inputmethod.InputMethodSubtype;
 import android.widget.FrameLayout;
 import android.widget.TextView;
 
 import com.android.inputmethod.keyboard.Key;
 import com.android.inputmethod.keyboard.Keyboard;
 
-import java.util.HashMap;
 import java.util.Locale;
 
 public class InputTestsBase extends ServiceTestCase<LatinIME> {
diff --git a/tests/src/com/android/inputmethod/latin/PunctuationTests.java b/tests/src/com/android/inputmethod/latin/PunctuationTests.java
index 0eb3ba4..38203e8 100644
--- a/tests/src/com/android/inputmethod/latin/PunctuationTests.java
+++ b/tests/src/com/android/inputmethod/latin/PunctuationTests.java
@@ -16,8 +16,11 @@
 
 package com.android.inputmethod.latin;
 
+import android.test.suitebuilder.annotation.LargeTest;
+
 import com.android.inputmethod.latin.R;
 
+@LargeTest
 public class PunctuationTests extends InputTestsBase {
 
     final String NEXT_WORD_PREDICTION_OPTION = "next_word_prediction";
diff --git a/tests/src/com/android/inputmethod/latin/ResizableIntArrayTests.java b/tests/src/com/android/inputmethod/latin/ResizableIntArrayTests.java
index 995fc14..7c415df 100644
--- a/tests/src/com/android/inputmethod/latin/ResizableIntArrayTests.java
+++ b/tests/src/com/android/inputmethod/latin/ResizableIntArrayTests.java
@@ -17,7 +17,9 @@
 package com.android.inputmethod.latin;
 
 import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
 
+@SmallTest
 public class ResizableIntArrayTests extends AndroidTestCase {
     private static final int DEFAULT_CAPACITY = 48;
 
diff --git a/tests/src/com/android/inputmethod/latin/RichInputConnectionTests.java b/tests/src/com/android/inputmethod/latin/RichInputConnectionTests.java
index ad99379..9b8fc6d 100644
--- a/tests/src/com/android/inputmethod/latin/RichInputConnectionTests.java
+++ b/tests/src/com/android/inputmethod/latin/RichInputConnectionTests.java
@@ -18,6 +18,7 @@
 
 import android.inputmethodservice.InputMethodService;
 import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
 import android.view.inputmethod.ExtractedText;
 import android.view.inputmethod.ExtractedTextRequest;
 import android.view.inputmethod.InputConnection;
@@ -25,6 +26,7 @@
 
 import com.android.inputmethod.latin.RichInputConnection.Range;
 
+@SmallTest
 public class RichInputConnectionTests extends AndroidTestCase {
 
     // The following is meant to be a reasonable default for
diff --git a/tests/src/com/android/inputmethod/latin/StringUtilsTests.java b/tests/src/com/android/inputmethod/latin/StringUtilsTests.java
index 3142bd6..f3fbb3e 100644
--- a/tests/src/com/android/inputmethod/latin/StringUtilsTests.java
+++ b/tests/src/com/android/inputmethod/latin/StringUtilsTests.java
@@ -17,10 +17,12 @@
 package com.android.inputmethod.latin;
 
 import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
 import android.text.TextUtils;
 
 import java.util.Locale;
 
+@SmallTest
 public class StringUtilsTests extends AndroidTestCase {
     public void testContainsInArray() {
         assertFalse("empty array", StringUtils.containsInArray("key", new String[0]));
diff --git a/tests/src/com/android/inputmethod/latin/SubtypeLocaleTests.java b/tests/src/com/android/inputmethod/latin/SubtypeLocaleTests.java
index e37fef7..e406e84 100644
--- a/tests/src/com/android/inputmethod/latin/SubtypeLocaleTests.java
+++ b/tests/src/com/android/inputmethod/latin/SubtypeLocaleTests.java
@@ -19,6 +19,7 @@
 import android.content.Context;
 import android.content.res.Resources;
 import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
 import android.view.inputmethod.InputMethodSubtype;
 
 import com.android.inputmethod.latin.LocaleUtils.RunInLocale;
@@ -26,6 +27,7 @@
 import java.util.ArrayList;
 import java.util.Locale;
 
+@SmallTest
 public class SubtypeLocaleTests extends AndroidTestCase {
     // Locale to subtypes list.
     private final ArrayList<InputMethodSubtype> mSubtypesList = CollectionUtils.newArrayList();
diff --git a/tests/src/com/android/inputmethod/latin/UserHistoryDictIOUtilsTests.java b/tests/src/com/android/inputmethod/latin/UserHistoryDictIOUtilsTests.java
index 66c9e18..dd28fab 100644
--- a/tests/src/com/android/inputmethod/latin/UserHistoryDictIOUtilsTests.java
+++ b/tests/src/com/android/inputmethod/latin/UserHistoryDictIOUtilsTests.java
@@ -16,17 +16,17 @@
 
 package com.android.inputmethod.latin;
 
+import android.content.Context;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+
 import com.android.inputmethod.latin.UserHistoryDictIOUtils.BigramDictionaryInterface;
 import com.android.inputmethod.latin.UserHistoryDictIOUtils.OnAddWordListener;
-import com.android.inputmethod.latin.makedict.BinaryDictInputOutput;
 import com.android.inputmethod.latin.makedict.FormatSpec;
 import com.android.inputmethod.latin.makedict.FusionDictionary;
 import com.android.inputmethod.latin.makedict.FusionDictionary.CharGroup;
 
-import android.content.Context;
-import android.test.AndroidTestCase;
-import android.util.Log;
-
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
@@ -39,6 +39,7 @@
 /**
  * Unit tests for UserHistoryDictIOUtils
  */
+@LargeTest
 public class UserHistoryDictIOUtilsTests extends AndroidTestCase
     implements BigramDictionaryInterface {
 
diff --git a/tests/src/com/android/inputmethod/latin/UserHistoryDictionaryTests.java b/tests/src/com/android/inputmethod/latin/UserHistoryDictionaryTests.java
index 7536f64..594ba0e 100644
--- a/tests/src/com/android/inputmethod/latin/UserHistoryDictionaryTests.java
+++ b/tests/src/com/android/inputmethod/latin/UserHistoryDictionaryTests.java
@@ -16,15 +16,13 @@
 
 package com.android.inputmethod.latin;
 
-import com.android.inputmethod.latin.UserHistoryDictionary;
-
 import android.content.SharedPreferences;
 import android.preference.PreferenceManager;
 import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
 import android.util.Log;
 
 import java.io.File;
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Random;
@@ -33,6 +31,7 @@
 /**
  * Unit tests for UserHistoryDictionary
  */
+@LargeTest
 public class UserHistoryDictionaryTests extends AndroidTestCase {
     private static final String TAG = UserHistoryDictionaryTests.class.getSimpleName();
     private SharedPreferences mPrefs;
diff --git a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictIOTests.java b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictIOTests.java
index d080ed6..f3694ea 100644
--- a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictIOTests.java
+++ b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictIOTests.java
@@ -16,6 +16,12 @@
 
 package com.android.inputmethod.latin.makedict;
 
+import android.test.AndroidTestCase;
+import android.test.MoreAsserts;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+import android.util.SparseArray;
+
 import com.android.inputmethod.latin.CollectionUtils;
 import com.android.inputmethod.latin.UserHistoryDictIOUtils;
 import com.android.inputmethod.latin.makedict.BinaryDictInputOutput.FusionDictionaryBufferInterface;
@@ -24,11 +30,6 @@
 import com.android.inputmethod.latin.makedict.FusionDictionary.Node;
 import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
 
-import android.test.AndroidTestCase;
-import android.test.MoreAsserts;
-import android.util.Log;
-import android.util.SparseArray;
-
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
@@ -47,6 +48,7 @@
 /**
  * Unit tests for BinaryDictInputOutput
  */
+@LargeTest
 public class BinaryDictIOTests extends AndroidTestCase {
     private static final String TAG = BinaryDictIOTests.class.getSimpleName();
     private static final int MAX_UNIGRAMS = 1000;
diff --git a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtilsTests.java b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtilsTests.java
index 3185168..25575ba 100644
--- a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtilsTests.java
+++ b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtilsTests.java
@@ -16,32 +16,30 @@
 
 package com.android.inputmethod.latin.makedict;
 
+import android.test.AndroidTestCase;
+import android.test.MoreAsserts;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+
 import com.android.inputmethod.latin.CollectionUtils;
 import com.android.inputmethod.latin.makedict.BinaryDictInputOutput.ByteBufferWrapper;
-import com.android.inputmethod.latin.makedict.BinaryDictInputOutput.CharEncoding;
 import com.android.inputmethod.latin.makedict.BinaryDictInputOutput.FusionDictionaryBufferInterface;
 import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader;
 import com.android.inputmethod.latin.makedict.FusionDictionary.Node;
 import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
 
-import android.test.AndroidTestCase;
-import android.test.MoreAsserts;
-import android.util.Log;
-
 import java.io.BufferedOutputStream;
-import java.io.BufferedWriter;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
-import java.io.FileWriter;
 import java.io.IOException;
 import java.io.RandomAccessFile;
 import java.nio.channels.FileChannel;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Random;
 
+@LargeTest
 public class BinaryDictIOUtilsTests  extends AndroidTestCase {
     private static final String TAG = BinaryDictIOUtilsTests.class.getSimpleName();
     private static final FormatSpec.FormatOptions FORMAT_OPTIONS =
diff --git a/tests/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerServiceTest.java b/tests/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerServiceTest.java
index 21406d3..1ea6b2e 100644
--- a/tests/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerServiceTest.java
+++ b/tests/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerServiceTest.java
@@ -16,12 +16,12 @@
 
 package com.android.inputmethod.latin.spellcheck;
 
-import android.text.SpannableStringBuilder;
-import android.text.style.CharacterStyle;
+import android.test.suitebuilder.annotation.LargeTest;
 import android.text.style.SuggestionSpan;
 
 import com.android.inputmethod.latin.InputTestsBase;
 
+@LargeTest
 public class AndroidSpellCheckerServiceTest extends InputTestsBase {
     public void testSpellchecker() {
         mTextView.onAttachedToWindow();
diff --git a/tests/src/com/android/inputmethod/research/MotionEventReaderTests.java b/tests/src/com/android/inputmethod/research/MotionEventReaderTests.java
new file mode 100644
index 0000000..e5b9bc0
--- /dev/null
+++ b/tests/src/com/android/inputmethod/research/MotionEventReaderTests.java
@@ -0,0 +1,171 @@
+/*
+ * 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.research;
+
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.util.JsonReader;
+
+import com.android.inputmethod.research.MotionEventReader.ReplayData;
+
+import java.io.IOException;
+import java.io.StringReader;
+
+@SmallTest
+public class MotionEventReaderTests extends AndroidTestCase {
+    private MotionEventReader mMotionEventReader = new MotionEventReader();
+    private ReplayData mReplayData;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mReplayData = new ReplayData();
+    }
+
+    private JsonReader jsonReaderForString(final String s) {
+        return new JsonReader(new StringReader(s));
+    }
+
+    public void testTopLevelDataVariant() {
+        final JsonReader jsonReader = jsonReaderForString(
+                "{"
+                + "\"_ct\": 1359590400000,"
+                + "\"_ut\": 4381933,"
+                + "\"_ty\": \"MotionEvent\","
+                + "\"action\": \"UP\","
+                + "\"isLoggingRelated\": false,"
+                + "\"x\": 100.0,"
+                + "\"y\": 200.0"
+                + "}"
+                );
+        try {
+            mMotionEventReader.readLogStatement(jsonReader, mReplayData);
+        } catch (IOException e) {
+            e.printStackTrace();
+            fail("IOException thrown");
+        }
+        assertEquals("x set correctly", (int) mReplayData.mPointerCoordsArrays.get(0)[0].x, 100);
+        assertEquals("y set correctly", (int) mReplayData.mPointerCoordsArrays.get(0)[0].y, 200);
+        assertEquals("only one pointer", mReplayData.mPointerCoordsArrays.get(0).length, 1);
+        assertEquals("only one MotionEvent", mReplayData.mPointerCoordsArrays.size(), 1);
+    }
+
+    public void testNestedDataVariant() {
+        final JsonReader jsonReader = jsonReaderForString(
+                "{"
+                + "  \"_ct\": 135959040000,"
+                + "  \"_ut\": 4382702,"
+                + "  \"_ty\": \"MotionEvent\","
+                + "  \"action\": \"MOVE\","
+                + "  \"isLoggingRelated\": false,"
+                + "  \"motionEvent\": {"
+                + "    \"pointerIds\": ["
+                + "      0"
+                + "    ],"
+                + "    \"xyt\": ["
+                + "      {"
+                + "        \"t\": 4382551,"
+                + "        \"d\": ["
+                + "          {"
+                + "            \"x\": 100.0,"
+                + "            \"y\": 200.0,"
+                + "            \"toma\": 999.0,"
+                + "            \"tomi\": 999.0,"
+                + "            \"o\": 0.0"
+                + "          }"
+                + "        ]"
+                + "      },"
+                + "      {"
+                + "        \"t\": 4382559,"
+                + "        \"d\": ["
+                + "          {"
+                + "            \"x\": 300.0,"
+                + "            \"y\": 400.0,"
+                + "            \"toma\": 999.0,"
+                + "            \"tomi\": 999.0,"
+                + "            \"o\": 0.0"
+                + "          }"
+                + "        ]"
+                + "      }"
+                + "    ]"
+                + "  }"
+                + "}"
+                );
+        try {
+            mMotionEventReader.readLogStatement(jsonReader, mReplayData);
+        } catch (IOException e) {
+            e.printStackTrace();
+            fail("IOException thrown");
+        }
+        assertEquals("x1 set correctly", (int) mReplayData.mPointerCoordsArrays.get(0)[0].x, 100);
+        assertEquals("y1 set correctly", (int) mReplayData.mPointerCoordsArrays.get(0)[0].y, 200);
+        assertEquals("x2 set correctly", (int) mReplayData.mPointerCoordsArrays.get(1)[0].x, 300);
+        assertEquals("y2 set correctly", (int) mReplayData.mPointerCoordsArrays.get(1)[0].y, 400);
+        assertEquals("only one pointer", mReplayData.mPointerCoordsArrays.get(0).length, 1);
+        assertEquals("two MotionEvents", mReplayData.mPointerCoordsArrays.size(), 2);
+    }
+
+    public void testNestedDataVariantMultiPointer() {
+        final JsonReader jsonReader = jsonReaderForString(
+                "{"
+                + "  \"_ct\": 135959040000,"
+                + "  \"_ut\": 4382702,"
+                + "  \"_ty\": \"MotionEvent\","
+                + "  \"action\": \"MOVE\","
+                + "  \"isLoggingRelated\": false,"
+                + "  \"motionEvent\": {"
+                + "    \"pointerIds\": ["
+                + "      1"
+                + "    ],"
+                + "    \"xyt\": ["
+                + "      {"
+                + "        \"t\": 4382551,"
+                + "        \"d\": ["
+                + "          {"
+                + "            \"x\": 100.0,"
+                + "            \"y\": 200.0,"
+                + "            \"toma\": 999.0,"
+                + "            \"tomi\": 999.0,"
+                + "            \"o\": 0.0"
+                + "          },"
+                + "          {"
+                + "            \"x\": 300.0,"
+                + "            \"y\": 400.0,"
+                + "            \"toma\": 999.0,"
+                + "            \"tomi\": 999.0,"
+                + "            \"o\": 0.0"
+                + "          }"
+                + "        ]"
+                + "      }"
+                + "    ]"
+                + "  }"
+                + "}"
+                );
+        try {
+            mMotionEventReader.readLogStatement(jsonReader, mReplayData);
+        } catch (IOException e) {
+            e.printStackTrace();
+            fail("IOException thrown");
+        }
+        assertEquals("x1 set correctly", (int) mReplayData.mPointerCoordsArrays.get(0)[0].x, 100);
+        assertEquals("y1 set correctly", (int) mReplayData.mPointerCoordsArrays.get(0)[0].y, 200);
+        assertEquals("x2 set correctly", (int) mReplayData.mPointerCoordsArrays.get(0)[1].x, 300);
+        assertEquals("y2 set correctly", (int) mReplayData.mPointerCoordsArrays.get(0)[1].y, 400);
+        assertEquals("two pointers", mReplayData.mPointerCoordsArrays.get(0).length, 2);
+        assertEquals("one MotionEvent", mReplayData.mPointerCoordsArrays.size(), 1);
+    }
+}