Merge "Differentiate LOG_EVERYTHING and LOG_FULL_TEXTVIEW_CONTENTS"
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index f0705a8..7dfaf6c 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -1681,6 +1681,9 @@
             if (length > 0) {
                 if (mWordComposer.isBatchMode()) {
                     mWordComposer.reset();
+                    if (ProductionFlag.IS_EXPERIMENTAL) {
+                        ResearchLogger.latinIME_handleBackspace_batch(mWordComposer.getTypedWord());
+                    }
                 } else {
                     mWordComposer.deleteLast();
                 }
diff --git a/java/src/com/android/inputmethod/research/LogBuffer.java b/java/src/com/android/inputmethod/research/LogBuffer.java
index ae7b157..a3c3e89 100644
--- a/java/src/com/android/inputmethod/research/LogBuffer.java
+++ b/java/src/com/android/inputmethod/research/LogBuffer.java
@@ -110,4 +110,8 @@
         }
         return logUnit;
     }
+
+    public boolean isEmpty() {
+        return mLogUnits.isEmpty();
+    }
 }
diff --git a/java/src/com/android/inputmethod/research/LogUnit.java b/java/src/com/android/inputmethod/research/LogUnit.java
index 884ade0..27c4027 100644
--- a/java/src/com/android/inputmethod/research/LogUnit.java
+++ b/java/src/com/android/inputmethod/research/LogUnit.java
@@ -24,10 +24,12 @@
 import com.android.inputmethod.keyboard.Key;
 import com.android.inputmethod.latin.SuggestedWords;
 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+import com.android.inputmethod.latin.Utils;
 import com.android.inputmethod.latin.define.ProductionFlag;
 import com.android.inputmethod.research.ResearchLogger.LogStatement;
 
 import java.io.IOException;
+import java.io.StringWriter;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
@@ -95,6 +97,22 @@
      */
     public synchronized void publishTo(final ResearchLog researchLog,
             final boolean isIncludingPrivateData) {
+        // Prepare debugging output if necessary
+        final StringWriter debugStringWriter;
+        final JsonWriter debugJsonWriter;
+        if (DEBUG) {
+            debugStringWriter = new StringWriter();
+            debugJsonWriter = new JsonWriter(debugStringWriter);
+            debugJsonWriter.setIndent("  ");
+            try {
+                debugJsonWriter.beginArray();
+            } catch (IOException e) {
+                Log.e(TAG, "Could not open array in JsonWriter", e);
+            }
+        } else {
+            debugStringWriter = null;
+            debugJsonWriter = null;
+        }
         final int size = mLogStatementList.size();
         // Write out any logStatement that passes the privacy filter.
         for (int i = 0; i < size; i++) {
@@ -110,6 +128,23 @@
             final JsonWriter jsonWriter = researchLog.getValidJsonWriterLocked();
             outputLogStatementToLocked(jsonWriter, mLogStatementList.get(i), mValuesList.get(i),
                     mTimeList.get(i));
+            if (DEBUG) {
+                outputLogStatementToLocked(debugJsonWriter, mLogStatementList.get(i),
+                        mValuesList.get(i), mTimeList.get(i));
+            }
+        }
+        if (DEBUG) {
+            try {
+                debugJsonWriter.endArray();
+                debugJsonWriter.flush();
+            } catch (IOException e) {
+                Log.e(TAG, "Could not close array in JsonWriter", e);
+            }
+            final String bigString = debugStringWriter.getBuffer().toString();
+            final String[] lines = bigString.split("\n");
+            for (String line : lines) {
+                Log.d(TAG, line);
+            }
         }
     }
 
diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java
index d82ccd1..ab436c7 100644
--- a/java/src/com/android/inputmethod/research/ResearchLogger.java
+++ b/java/src/com/android/inputmethod/research/ResearchLogger.java
@@ -34,7 +34,6 @@
 import android.graphics.Color;
 import android.graphics.Paint;
 import android.graphics.Paint.Style;
-import android.inputmethodservice.InputMethodService;
 import android.net.Uri;
 import android.os.Build;
 import android.os.IBinder;
@@ -147,7 +146,7 @@
     // used to check whether words are not unique
     private Suggest mSuggest;
     private MainKeyboardView mMainKeyboardView;
-    private InputMethodService mInputMethodService;
+    private LatinIME mLatinIME;
     private final Statistics mStatistics;
 
     private Intent mUploadIntent;
@@ -162,12 +161,12 @@
         return sInstance;
     }
 
-    public void init(final InputMethodService ims, final SharedPreferences prefs) {
-        assert ims != null;
-        if (ims == null) {
+    public void init(final LatinIME latinIME, final SharedPreferences prefs) {
+        assert latinIME != null;
+        if (latinIME == null) {
             Log.w(TAG, "IMS is null; logging is off");
         } else {
-            mFilesDir = ims.getFilesDir();
+            mFilesDir = latinIME.getFilesDir();
             if (mFilesDir == null || !mFilesDir.exists()) {
                 Log.w(TAG, "IME storage directory does not exist.");
             }
@@ -192,12 +191,12 @@
                 e.apply();
             }
         }
-        mInputMethodService = ims;
+        mLatinIME = latinIME;
         mPrefs = prefs;
-        mUploadIntent = new Intent(mInputMethodService, UploaderService.class);
+        mUploadIntent = new Intent(mLatinIME, UploaderService.class);
 
         if (ProductionFlag.IS_EXPERIMENTAL) {
-            scheduleUploadingService(mInputMethodService);
+            scheduleUploadingService(mLatinIME);
         }
     }
 
@@ -256,7 +255,7 @@
         if (windowToken == null) {
             return;
         }
-        final AlertDialog.Builder builder = new AlertDialog.Builder(mInputMethodService)
+        final AlertDialog.Builder builder = new AlertDialog.Builder(mLatinIME)
                 .setTitle(R.string.research_splash_title)
                 .setMessage(R.string.research_splash_content)
                 .setPositiveButton(android.R.string.yes,
@@ -271,12 +270,12 @@
                         new DialogInterface.OnClickListener() {
                             @Override
                             public void onClick(DialogInterface dialog, int which) {
-                                final String packageName = mInputMethodService.getPackageName();
+                                final String packageName = mLatinIME.getPackageName();
                                 final Uri packageUri = Uri.parse("package:" + packageName);
                                 final Intent intent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE,
                                         packageUri);
                                 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-                                mInputMethodService.startActivity(intent);
+                                mLatinIME.startActivity(intent);
                             }
                 })
                 .setCancelable(true)
@@ -284,7 +283,7 @@
                         new OnCancelListener() {
                             @Override
                             public void onCancel(DialogInterface dialog) {
-                                mInputMethodService.requestHideSelf(0);
+                                mLatinIME.requestHideSelf(0);
                             }
                 });
         mSplashDialog = builder.create();
@@ -328,10 +327,10 @@
     }
 
     private void checkForEmptyEditor() {
-        if (mInputMethodService == null) {
+        if (mLatinIME == null) {
             return;
         }
-        final InputConnection ic = mInputMethodService.getCurrentInputConnection();
+        final InputConnection ic = mLatinIME.getCurrentInputConnection();
         if (ic == null) {
             return;
         }
@@ -384,7 +383,6 @@
         if (DEBUG) {
             Log.d(TAG, "stop called");
         }
-        logStatistics();
         commitCurrentLogUnit();
 
         if (mMainLogBuffer != null) {
@@ -592,7 +590,7 @@
         if (DEBUG) {
             Log.d(TAG, "calling uploadNow()");
         }
-        mInputMethodService.startService(mUploadIntent);
+        mLatinIME.startService(mUploadIntent);
     }
 
     public void onLeavingSendFeedbackDialog() {
@@ -712,6 +710,7 @@
     /* package for test */ void publishLogBuffer(final LogBuffer logBuffer,
             final ResearchLog researchLog, final boolean isIncludingPrivateData) {
         final LogUnit openingLogUnit = new LogUnit();
+        if (logBuffer.isEmpty()) return;
         openingLogUnit.addLogStatement(LOGSTATEMENT_LOG_SEGMENT_OPENING, SystemClock.uptimeMillis(),
                 isIncludingPrivateData);
         researchLog.publish(openingLogUnit, true /* isIncludingPrivateData */);
@@ -812,6 +811,21 @@
         return WORD_REPLACEMENT_STRING;
     }
 
+    // Specific logging methods follow below.  The comments for each logging method should
+    // indicate what specific method is logged, and how to trigger it from the user interface.
+    //
+    // Logging methods can be generally classified into two flavors, "UserAction", which should
+    // correspond closely to an event that is sensed by the IME, and is usually generated
+    // directly by the user, and "SystemResponse" which corresponds to an event that the IME
+    // generates, often after much processing of user input.  SystemResponses should correspond
+    // closely to user-visible events.
+    // TODO: Consider exposing the UserAction classification in the log output.
+
+    /**
+     * Log a call to LatinIME.onStartInputViewInternal().
+     *
+     * UserAction: called each time the keyboard is opened up.
+     */
     private static final LogStatement LOGSTATEMENT_LATIN_IME_ON_START_INPUT_VIEW_INTERNAL =
             new LogStatement("LatinImeOnStartInputViewInternal", false, false, "uuid",
                     "packageName", "inputType", "imeOptions", "fieldId", "display", "model",
@@ -825,7 +839,7 @@
                     || InputTypeUtils.isVisiblePasswordInputType(editorInfo.inputType);
             getInstance().setIsPasswordView(isPassword);
             researchLogger.start();
-            final Context context = researchLogger.mInputMethodService;
+            final Context context = researchLogger.mLatinIME;
             try {
                 final PackageInfo packageInfo;
                 packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(),
@@ -846,9 +860,15 @@
     }
 
     public void latinIME_onFinishInputViewInternal() {
+        logStatistics();
         stop();
     }
 
+    /**
+     * Log a change in preferences.
+     *
+     * UserAction: called when the user changes the settings.
+     */
     private static final LogStatement LOGSTATEMENT_PREFS_CHANGED =
             new LogStatement("PrefsChanged", false, false, "prefs");
     public static void prefsChanged(final SharedPreferences prefs) {
@@ -856,8 +876,12 @@
         researchLogger.enqueueEvent(LOGSTATEMENT_PREFS_CHANGED, prefs);
     }
 
-    // Regular logging methods
-
+    /**
+     * Log a call to MainKeyboardView.processMotionEvent().
+     *
+     * UserAction: called when the user puts their finger onto the screen (ACTION_DOWN).
+     *
+     */
     private static final LogStatement LOGSTATEMENT_MAIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENT =
             new LogStatement("MainKeyboardViewProcessMotionEvent", true, false, "action",
                     "eventTime", "id", "x", "y", "size", "pressure");
@@ -882,6 +906,12 @@
         }
     }
 
+    /**
+     * Log a call to LatinIME.onCodeInput().
+     *
+     * SystemResponse: The main processing step for entering text.  Called when the user performs a
+     * tap, a flick, a long press, releases a gesture, or taps a punctuation suggestion.
+     */
     private static final LogStatement LOGSTATEMENT_LATIN_IME_ON_CODE_INPUT =
             new LogStatement("LatinImeOnCodeInput", true, false, "code", "x", "y");
     public static void latinIME_onCodeInput(final int code, final int x, final int y) {
@@ -894,6 +924,12 @@
         }
         researchLogger.mStatistics.recordChar(code, time);
     }
+    /**
+     * Log a call to LatinIME.onDisplayCompletions().
+     *
+     * SystemResponse: The IME has displayed application-specific completions.  They may show up
+     * in the suggestion strip, such as a landscape phone.
+     */
     private static final LogStatement LOGSTATEMENT_LATINIME_ONDISPLAYCOMPLETIONS =
             new LogStatement("LatinIMEOnDisplayCompletions", true, true,
                     "applicationSpecifiedCompletions");
@@ -911,6 +947,12 @@
         return returnValue;
     }
 
+    /**
+     * Log a call to LatinIME.onWindowHidden().
+     *
+     * UserAction: The user has performed an action that has caused the IME to be closed.  They may
+     * have focused on something other than a text field, or explicitly closed it.
+     */
     private static final LogStatement LOGSTATEMENT_LATINIME_ONWINDOWHIDDEN =
             new LogStatement("LatinIMEOnWindowHidden", false, false, "isTextTruncated", "text");
     public static void latinIME_onWindowHidden(final int savedSelectionStart,
@@ -966,6 +1008,12 @@
         }
     }
 
+    /**
+     * Log a call to LatinIME.onUpdateSelection().
+     *
+     * UserAction/SystemResponse: The user has moved the cursor or selection.  This function may
+     * be called, however, when the system has moved the cursor, say by inserting a character.
+     */
     private static final LogStatement LOGSTATEMENT_LATINIME_ONUPDATESELECTION =
             new LogStatement("LatinIMEOnUpdateSelection", true, false, "lastSelectionStart",
                     "lastSelectionEnd", "oldSelStart", "oldSelEnd", "newSelStart", "newSelEnd",
@@ -992,6 +1040,11 @@
                 expectingUpdateSelectionFromLogger, scrubbedWord);
     }
 
+    /**
+     * Log a call to LatinIME.pickSuggestionManually().
+     *
+     * UserAction: The user has chosen a specific word from the suggestion strip.
+     */
     private static final LogStatement LOGSTATEMENT_LATINIME_PICKSUGGESTIONMANUALLY =
             new LogStatement("LatinIMEPickSuggestionManually", true, false, "replacedWord", "index",
                     "suggestion", "x", "y");
@@ -1004,6 +1057,11 @@
                 Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE);
     }
 
+    /**
+     * Log a call to LatinIME.punctuationSuggestion().
+     *
+     * UserAction: The user has chosen punctuation from the punctuation suggestion strip.
+     */
     private static final LogStatement LOGSTATEMENT_LATINIME_PUNCTUATIONSUGGESTION =
             new LogStatement("LatinIMEPunctuationSuggestion", false, false, "index", "suggestion",
                     "x", "y");
@@ -1013,6 +1071,13 @@
                 Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE);
     }
 
+    /**
+     * Log a call to LatinIME.sendKeyCodePoint().
+     *
+     * SystemResponse: The IME is simulating a hardware keypress.  This happens for numbers; other
+     * input typically goes through RichInputConnection.setComposingText() and
+     * RichInputConnection.commitText().
+     */
     private static final LogStatement LOGSTATEMENT_LATINIME_SENDKEYCODEPOINT =
             new LogStatement("LatinIMESendKeyCodePoint", true, false, "code");
     public static void latinIME_sendKeyCodePoint(final int code) {
@@ -1024,18 +1089,36 @@
         }
     }
 
+    /**
+     * Log a call to LatinIME.swapSwapperAndSpace().
+     *
+     * SystemResponse: A symbol has been swapped with a space character.  E.g. punctuation may swap
+     * if a soft space is inserted after a word.
+     */
     private static final LogStatement LOGSTATEMENT_LATINIME_SWAPSWAPPERANDSPACE =
             new LogStatement("LatinIMESwapSwapperAndSpace", false, false);
     public static void latinIME_swapSwapperAndSpace() {
         getInstance().enqueueEvent(LOGSTATEMENT_LATINIME_SWAPSWAPPERANDSPACE);
     }
 
+    /**
+     * Log a call to MainKeyboardView.onLongPress().
+     *
+     * UserAction: The user has performed a long-press on a key.
+     */
     private static final LogStatement LOGSTATEMENT_MAINKEYBOARDVIEW_ONLONGPRESS =
             new LogStatement("MainKeyboardViewOnLongPress", false, false);
     public static void mainKeyboardView_onLongPress() {
         getInstance().enqueueEvent(LOGSTATEMENT_MAINKEYBOARDVIEW_ONLONGPRESS);
     }
 
+    /**
+     * Log a call to MainKeyboardView.setKeyboard().
+     *
+     * SystemResponse: The IME has switched to a new keyboard (e.g. French, English).
+     * This is typically called right after LatinIME.onStartInputViewInternal (when starting a new
+     * IME), but may happen at other times if the user explicitly requests a keyboard change.
+     */
     private static final LogStatement LOGSTATEMENT_MAINKEYBOARDVIEW_SETKEYBOARD =
             new LogStatement("MainKeyboardViewSetKeyboard", false, false, "elementId", "locale",
                     "orientation", "width", "modeName", "action", "navigateNext",
@@ -1056,18 +1139,38 @@
                 keyboard.mOccupiedHeight, keyboard.mKeys);
     }
 
+    /**
+     * Log a call to LatinIME.revertCommit().
+     *
+     * SystemResponse: The IME has reverted commited text.  This happens when the user enters
+     * a word, commits it by pressing space or punctuation, and then reverts the commit by hitting
+     * backspace.
+     */
     private static final LogStatement LOGSTATEMENT_LATINIME_REVERTCOMMIT =
             new LogStatement("LatinIMERevertCommit", true, false, "originallyTypedWord");
     public static void latinIME_revertCommit(final String originallyTypedWord) {
         getInstance().enqueueEvent(LOGSTATEMENT_LATINIME_REVERTCOMMIT, originallyTypedWord);
     }
 
+    /**
+     * Log a call to PointerTracker.callListenerOnCancelInput().
+     *
+     * UserAction: The user has canceled the input, e.g., by pressing down, but then removing
+     * outside the keyboard area.
+     * TODO: Verify
+     */
     private static final LogStatement LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCANCELINPUT =
             new LogStatement("PointerTrackerCallListenerOnCancelInput", false, false);
     public static void pointerTracker_callListenerOnCancelInput() {
         getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCANCELINPUT);
     }
 
+    /**
+     * Log a call to PointerTracker.callListenerOnCodeInput().
+     *
+     * SystemResponse: The user has entered a key through the normal tapping mechanism.
+     * LatinIME.onCodeInput will also be called.
+     */
     private static final LogStatement LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCODEINPUT =
             new LogStatement("PointerTrackerCallListenerOnCodeInput", true, false, "code",
                     "outputText", "x", "y", "ignoreModifierKey", "altersCode", "isEnabled");
@@ -1083,6 +1186,11 @@
         }
     }
 
+    /**
+     * Log a call to PointerTracker.callListenerCallListenerOnRelease().
+     *
+     * UserAction: The user has released their finger or thumb from the screen.
+     */
     private static final LogStatement LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONRELEASE =
             new LogStatement("PointerTrackerCallListenerOnRelease", true, false, "code",
                     "withSliding", "ignoreModifierKey", "isEnabled");
@@ -1095,6 +1203,12 @@
         }
     }
 
+    /**
+     * Log a call to PointerTracker.onDownEvent().
+     *
+     * UserAction: The user has pressed down on a key.
+     * TODO: Differentiate with LatinIME.processMotionEvent.
+     */
     private static final LogStatement LOGSTATEMENT_POINTERTRACKER_ONDOWNEVENT =
             new LogStatement("PointerTrackerOnDownEvent", true, false, "deltaT", "distanceSquared");
     public static void pointerTracker_onDownEvent(long deltaT, int distanceSquared) {
@@ -1102,6 +1216,12 @@
                 distanceSquared);
     }
 
+    /**
+     * Log a call to PointerTracker.onMoveEvent().
+     *
+     * UserAction: The user has moved their finger while pressing on the screen.
+     * TODO: Differentiate with LatinIME.processMotionEvent().
+     */
     private static final LogStatement LOGSTATEMENT_POINTERTRACKER_ONMOVEEVENT =
             new LogStatement("PointerTrackerOnMoveEvent", true, false, "x", "y", "lastX", "lastY");
     public static void pointerTracker_onMoveEvent(final int x, final int y, final int lastX,
@@ -1109,6 +1229,12 @@
         getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_ONMOVEEVENT, x, y, lastX, lastY);
     }
 
+    /**
+     * Log a call to RichInputConnection.commitCompletion().
+     *
+     * SystemResponse: The IME has committed a completion.  A completion is an application-
+     * specific suggestion that is presented in a pop-up menu in the TextView.
+     */
     private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_COMMITCOMPLETION =
             new LogStatement("RichInputConnectionCommitCompletion", true, false, "completionInfo");
     public static void richInputConnection_commitCompletion(final CompletionInfo completionInfo) {
@@ -1126,25 +1252,40 @@
         researchLogger.isExpectingCommitText = true;
     }
 
-    private static final LogStatement LOGSTATEMENT_COMMITTEXT_UPDATECURSOR =
-            new LogStatement("CommitTextUpdateCursor", true, false, "newCursorPosition");
+    /**
+     * Log a call to RichInputConnection.commitText().
+     *
+     * SystemResponse: The IME is committing text.  This happens after the user has typed a word
+     * and then a space or punctuation key.
+     */
+    private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTIONCOMMITTEXT =
+            new LogStatement("RichInputConnectionCommitText", true, false, "newCursorPosition");
     public static void richInputConnection_commitText(final CharSequence committedWord,
             final int newCursorPosition) {
         final ResearchLogger researchLogger = getInstance();
         final String scrubbedWord = scrubDigitsFromString(committedWord.toString());
         if (!researchLogger.isExpectingCommitText) {
             researchLogger.onWordComplete(scrubbedWord, Long.MAX_VALUE, false /* isPartial */);
-            researchLogger.enqueueEvent(LOGSTATEMENT_COMMITTEXT_UPDATECURSOR, newCursorPosition);
+            researchLogger.enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTIONCOMMITTEXT,
+                    newCursorPosition);
         }
         researchLogger.isExpectingCommitText = false;
     }
 
+    /**
+     * Shared event for logging committed text.
+     */
     private static final LogStatement LOGSTATEMENT_COMMITTEXT =
             new LogStatement("CommitText", true, false, "committedText");
     private void enqueueCommitText(final CharSequence word) {
         enqueueEvent(LOGSTATEMENT_COMMITTEXT, word);
     }
 
+    /**
+     * Log a call to RichInputConnection.deleteSurroundingText().
+     *
+     * SystemResponse: The IME has deleted text.
+     */
     private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT =
             new LogStatement("RichInputConnectionDeleteSurroundingText", true, false,
                     "beforeLength", "afterLength");
@@ -1154,20 +1295,37 @@
                 beforeLength, afterLength);
     }
 
+    /**
+     * Log a call to RichInputConnection.finishComposingText().
+     *
+     * SystemResponse: The IME has left the composing text as-is.
+     */
     private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_FINISHCOMPOSINGTEXT =
             new LogStatement("RichInputConnectionFinishComposingText", false, false);
     public static void richInputConnection_finishComposingText() {
         getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_FINISHCOMPOSINGTEXT);
     }
 
+    /**
+     * Log a call to RichInputConnection.performEditorAction().
+     *
+     * SystemResponse: The IME is invoking an action specific to the editor.
+     */
     private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_PERFORMEDITORACTION =
             new LogStatement("RichInputConnectionPerformEditorAction", false, false,
-                    "imeActionNext");
-    public static void richInputConnection_performEditorAction(final int imeActionNext) {
+                    "imeActionId");
+    public static void richInputConnection_performEditorAction(final int imeActionId) {
         getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_PERFORMEDITORACTION,
-                imeActionNext);
+                imeActionId);
     }
 
+    /**
+     * Log a call to RichInputConnection.sendKeyEvent().
+     *
+     * SystemResponse: The IME is telling the TextView that a key is being pressed through an
+     * alternate channel.
+     * TODO: only for hardware keys?
+     */
     private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_SENDKEYEVENT =
             new LogStatement("RichInputConnectionSendKeyEvent", true, false, "eventTime", "action",
                     "code");
@@ -1176,6 +1334,12 @@
                 keyEvent.getEventTime(), keyEvent.getAction(), keyEvent.getKeyCode());
     }
 
+    /**
+     * Log a call to RichInputConnection.setComposingText().
+     *
+     * SystemResponse: The IME is setting the composing text.  Happens each time a character is
+     * entered.
+     */
     private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_SETCOMPOSINGTEXT =
             new LogStatement("RichInputConnectionSetComposingText", true, true, "text",
                     "newCursorPosition");
@@ -1188,12 +1352,24 @@
                 newCursorPosition);
     }
 
+    /**
+     * Log a call to RichInputConnection.setSelection().
+     *
+     * SystemResponse: The IME is requesting that the selection change.  User-initiated selection-
+     * change requests do not go through this method -- it's only when the system wants to change
+     * the selection.
+     */
     private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_SETSELECTION =
             new LogStatement("RichInputConnectionSetSelection", true, false, "from", "to");
     public static void richInputConnection_setSelection(final int from, final int to) {
         getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_SETSELECTION, from, to);
     }
 
+    /**
+     * Log a call to SuddenJumpingTouchEventHandler.onTouchEvent().
+     *
+     * SystemResponse: The IME has filtered input events in case of an erroneous sensor reading.
+     */
     private static final LogStatement LOGSTATEMENT_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT =
             new LogStatement("SuddenJumpingTouchEventHandlerOnTouchEvent", true, false,
                     "motionEvent");
@@ -1204,6 +1380,11 @@
         }
     }
 
+    /**
+     * Log a call to SuggestionsView.setSuggestions().
+     *
+     * SystemResponse: The IME is setting the suggestions in the suggestion strip.
+     */
     private static final LogStatement LOGSTATEMENT_SUGGESTIONSTRIPVIEW_SETSUGGESTIONS =
             new LogStatement("SuggestionStripViewSetSuggestions", true, true, "suggestedWords");
     public static void suggestionStripView_setSuggestions(final SuggestedWords suggestedWords) {
@@ -1213,12 +1394,22 @@
         }
     }
 
+    /**
+     * The user has indicated a particular point in the log that is of interest.
+     *
+     * UserAction: From direct menu invocation.
+     */
     private static final LogStatement LOGSTATEMENT_USER_TIMESTAMP =
             new LogStatement("UserTimestamp", false, false);
     public void userTimestamp() {
         getInstance().enqueueEvent(LOGSTATEMENT_USER_TIMESTAMP);
     }
 
+    /**
+     * Log a call to LatinIME.onEndBatchInput().
+     *
+     * SystemResponse: The system has completed a gesture.
+     */
     private static final LogStatement LOGSTATEMENT_LATINIME_ONENDBATCHINPUT =
             new LogStatement("LatinIMEOnEndBatchInput", true, false, "enteredText",
                     "enteredWordPos");
@@ -1227,15 +1418,34 @@
         final ResearchLogger researchLogger = getInstance();
         researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_ONENDBATCHINPUT, enteredText,
                 enteredWordPos);
-        researchLogger.mStatistics.recordGestureInput();
+        researchLogger.mStatistics.recordGestureInput(enteredText.length());
     }
 
+    /**
+     * Log a call to LatinIME.handleBackspace().
+     *
+     * UserInput: The user is deleting a gestured word by hitting the backspace key once.
+     */
+    private static final LogStatement LOGSTATEMENT_LATINIME_HANDLEBACKSPACE_BATCH =
+            new LogStatement("LatinIMEHandleBackspaceBatch", true, false, "deletedText");
+    public static void latinIME_handleBackspace_batch(final CharSequence deletedText) {
+        final ResearchLogger researchLogger = getInstance();
+        researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_HANDLEBACKSPACE_BATCH, deletedText);
+        researchLogger.mStatistics.recordGestureDelete();
+    }
+
+    /**
+     * Log statistics.
+     *
+     * ContextualData, recorded at the end of a session.
+     */
     private static final LogStatement LOGSTATEMENT_STATISTICS =
             new LogStatement("Statistics", false, false, "charCount", "letterCount", "numberCount",
                     "spaceCount", "deleteOpsCount", "wordCount", "isEmptyUponStarting",
                     "isEmptinessStateKnown", "averageTimeBetweenKeys", "averageTimeBeforeDelete",
                     "averageTimeDuringRepeatedDelete", "averageTimeAfterDelete",
-                    "dictionaryWordCount", "splitWordsCount", "gestureInputCount");
+                    "dictionaryWordCount", "splitWordsCount", "gestureInputCount",
+                    "gestureCharsCount", "gesturesDeletedCount");
     private static void logStatistics() {
         final ResearchLogger researchLogger = getInstance();
         final Statistics statistics = researchLogger.mStatistics;
@@ -1247,6 +1457,8 @@
                 statistics.mDuringRepeatedDeleteKeysCounter.getAverageTime(),
                 statistics.mAfterDeleteKeyCounter.getAverageTime(),
                 statistics.mDictionaryWordCount, statistics.mSplitWordsCount,
-                statistics.mGestureInputCount);
+                statistics.mGesturesInputCount,
+                statistics.mGesturesCharsCount,
+                statistics.mGesturesDeletedCount);
     }
 }
diff --git a/java/src/com/android/inputmethod/research/Statistics.java b/java/src/com/android/inputmethod/research/Statistics.java
index 23d1050..a02aabf 100644
--- a/java/src/com/android/inputmethod/research/Statistics.java
+++ b/java/src/com/android/inputmethod/research/Statistics.java
@@ -42,7 +42,11 @@
     // Number of words split and spaces automatically entered.
     int mSplitWordsCount;
     // Number of gestures that were input.
-    int mGestureInputCount;
+    int mGesturesInputCount;
+    // Number of gestures that were deleted.
+    int mGesturesDeletedCount;
+    // Total number of characters in words entered by gesture.
+    int mGesturesCharsCount;
     // Whether the text field was empty upon editing
     boolean mIsEmptyUponStarting;
     boolean mIsEmptinessStateKnown;
@@ -103,12 +107,18 @@
         mSpaceCount = 0;
         mDeleteKeyCount = 0;
         mWordCount = 0;
+        mDictionaryWordCount = 0;
+        mSplitWordsCount = 0;
+        mGesturesInputCount = 0;
+        mGesturesDeletedCount = 0;
         mIsEmptyUponStarting = true;
         mIsEmptinessStateKnown = false;
         mKeyCounter.reset();
         mBeforeDeleteKeyCounter.reset();
         mDuringRepeatedDeleteKeysCounter.reset();
         mAfterDeleteKeyCounter.reset();
+        mGesturesCharsCount = 0;
+        mGesturesDeletedCount = 0;
 
         mLastTapTime = 0;
         mIsLastKeyDeleteKey = false;
@@ -161,12 +171,17 @@
         mSplitWordsCount++;
     }
 
-    public void recordGestureInput() {
-        mGestureInputCount++;
+    public void recordGestureInput(final int numCharsEntered) {
+        mGesturesInputCount++;
+        mGesturesCharsCount += numCharsEntered;
     }
 
     public void setIsEmptyUponStarting(final boolean isEmpty) {
         mIsEmptyUponStarting = isEmpty;
         mIsEmptinessStateKnown = true;
     }
+
+    public void recordGestureDelete() {
+        mGesturesDeletedCount++;
+    }
 }
diff --git a/java/src/com/android/inputmethod/research/UploaderService.java b/java/src/com/android/inputmethod/research/UploaderService.java
index 26a0fab..a1ecc11 100644
--- a/java/src/com/android/inputmethod/research/UploaderService.java
+++ b/java/src/com/android/inputmethod/research/UploaderService.java
@@ -46,9 +46,10 @@
 
 public final class UploaderService extends IntentService {
     private static final String TAG = UploaderService.class.getSimpleName();
+    private static final boolean DEBUG = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG;
     // Set IS_INHIBITING_AUTO_UPLOAD to true for local testing
-    private static final boolean IS_INHIBITING_AUTO_UPLOAD =
-            false && ProductionFlag.IS_EXPERIMENTAL_DEBUG;  // Force false in production
+    private static final boolean IS_INHIBITING_AUTO_UPLOAD = false
+            && ProductionFlag.IS_EXPERIMENTAL_DEBUG;  // Force false in production
     public static final long RUN_INTERVAL = AlarmManager.INTERVAL_HOUR;
     private static final String EXTRA_UPLOAD_UNCONDITIONALLY = UploaderService.class.getName()
             + ".extra.UPLOAD_UNCONDITIONALLY";
@@ -146,7 +147,9 @@
     }
 
     private boolean uploadFile(File file) {
-        Log.d(TAG, "attempting upload of " + file.getAbsolutePath());
+        if (DEBUG) {
+            Log.d(TAG, "attempting upload of " + file.getAbsolutePath());
+        }
         boolean success = false;
         final int contentLength = (int) file.length();
         HttpURLConnection connection = null;
@@ -162,6 +165,9 @@
             int numBytesRead;
             while ((numBytesRead = fileInputStream.read(buf)) != -1) {
                 os.write(buf, 0, numBytesRead);
+                if (DEBUG) {
+                    Log.d(TAG, new String(buf));
+                }
             }
             if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
                 Log.d(TAG, "upload failed: " + connection.getResponseCode());
@@ -176,7 +182,9 @@
             }
             file.delete();
             success = true;
-            Log.d(TAG, "upload successful");
+            if (DEBUG) {
+                Log.d(TAG, "upload successful");
+            }
         } catch (Exception e) {
             e.printStackTrace();
         } finally {