ResearchLogger to track simple statistics

DO NOT MERGE

Bug: 6188932
Change-Id: I4667f8f60aa356c73f925c298318520f71e144c5
diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java
index 98ed536..b5bdc19 100644
--- a/java/src/com/android/inputmethod/research/ResearchLogger.java
+++ b/java/src/com/android/inputmethod/research/ResearchLogger.java
@@ -34,6 +34,7 @@
 import android.inputmethodservice.InputMethodService;
 import android.os.Build;
 import android.os.IBinder;
+import android.os.SystemClock;
 import android.text.TextUtils;
 import android.text.format.DateUtils;
 import android.util.Log;
@@ -137,10 +138,12 @@
     private Dictionary mDictionary;
     private KeyboardSwitcher mKeyboardSwitcher;
     private InputMethodService mInputMethodService;
+    private final Statistics mStatistics;
 
     private ResearchLogUploader mResearchLogUploader;
 
     private ResearchLogger() {
+        mStatistics = Statistics.getInstance();
     }
 
     public static ResearchLogger getInstance() {
@@ -270,10 +273,35 @@
         return new File(filesDir, sb.toString());
     }
 
+    private void checkForEmptyEditor() {
+        if (mInputMethodService == null) {
+            return;
+        }
+        final InputConnection ic = mInputMethodService.getCurrentInputConnection();
+        if (ic == null) {
+            return;
+        }
+        final CharSequence textBefore = ic.getTextBeforeCursor(1, 0);
+        if (!TextUtils.isEmpty(textBefore)) {
+            mStatistics.setIsEmptyUponStarting(false);
+            return;
+        }
+        final CharSequence textAfter = ic.getTextAfterCursor(1, 0);
+        if (!TextUtils.isEmpty(textAfter)) {
+            mStatistics.setIsEmptyUponStarting(false);
+            return;
+        }
+        if (textBefore != null && textAfter != null) {
+            mStatistics.setIsEmptyUponStarting(true);
+        }
+    }
+
     private void start() {
         maybeShowSplashScreen();
         updateSuspendedState();
         requestIndicatorRedraw();
+        mStatistics.reset();
+        checkForEmptyEditor();
         if (!isAllowedToLog()) {
             // Log.w(TAG, "not in usability mode; not logging");
             return;
@@ -297,6 +325,10 @@
     }
 
     /* package */ void stop() {
+        logStatistics();
+        publishLogUnit(mCurrentLogUnit, true);
+        mCurrentLogUnit = new LogUnit();
+
         if (mMainResearchLog != null) {
             mMainResearchLog.stop();
         }
@@ -305,6 +337,26 @@
         }
     }
 
+    private static final String[] EVENTKEYS_STATISTICS = {
+        "Statistics", "charCount", "letterCount", "numberCount", "spaceCount", "deleteOpsCount",
+        "wordCount", "isEmptyUponStarting", "isEmptinessStateKnown", "averageTimeBetweenKeys",
+        "averageTimeBeforeDelete", "averageTimeDuringRepeatedDelete", "averageTimeAfterDelete"
+    };
+    private static void logStatistics() {
+        final ResearchLogger researchLogger = getInstance();
+        final Statistics statistics = researchLogger.mStatistics;
+        final Object[] values = {
+                statistics.mCharCount, statistics.mLetterCount, statistics.mNumberCount,
+                statistics.mSpaceCount, statistics.mDeleteKeyCount,
+                statistics.mWordCount, statistics.mIsEmptyUponStarting,
+                statistics.mIsEmptinessStateKnown, statistics.mKeyCounter.getAverageTime(),
+                statistics.mBeforeDeleteKeyCounter.getAverageTime(),
+                statistics.mDuringRepeatedDeleteKeysCounter.getAverageTime(),
+                statistics.mAfterDeleteKeyCounter.getAverageTime()
+        };
+        researchLogger.enqueueEvent(EVENTKEYS_STATISTICS, values);
+    }
+
     private void setLoggingAllowed(boolean enableLogging) {
         if (mPrefs == null) {
             return;
@@ -705,6 +757,7 @@
             mLoggingFrequencyState.onWordLogged();
         }
         mCurrentLogUnit = new LogUnit();
+        mStatistics.recordWordEntered();
     }
 
     private void publishLogUnit(LogUnit logUnit, boolean isPrivacySensitive) {
@@ -899,7 +952,9 @@
         final Object[] values = {
             Keyboard.printableCode(scrubDigitFromCodePoint(code)), x, y
         };
-        getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_ONCODEINPUT, values);
+        final ResearchLogger researchLogger = getInstance();
+        researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_ONCODEINPUT, values);
+        researchLogger.mStatistics.recordChar(code, SystemClock.uptimeMillis());
     }
 
     private static final String[] EVENTKEYS_LATINIME_ONDISPLAYCOMPLETIONS = {
diff --git a/java/src/com/android/inputmethod/research/Statistics.java b/java/src/com/android/inputmethod/research/Statistics.java
new file mode 100644
index 0000000..4a2cd07
--- /dev/null
+++ b/java/src/com/android/inputmethod/research/Statistics.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2012 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 com.android.inputmethod.keyboard.Keyboard;
+
+public class Statistics {
+    // Number of characters entered during a typing session
+    int mCharCount;
+    // Number of letter characters entered during a typing session
+    int mLetterCount;
+    // Number of number characters entered
+    int mNumberCount;
+    // Number of space characters entered
+    int mSpaceCount;
+    // Number of delete operations entered (taps on the backspace key)
+    int mDeleteKeyCount;
+    // Number of words entered during a session.
+    int mWordCount;
+    // Whether the text field was empty upon editing
+    boolean mIsEmptyUponStarting;
+    boolean mIsEmptinessStateKnown;
+
+    // Timers to count average time to enter a key, first press a delete key,
+    // between delete keys, and then to return typing after a delete key.
+    final AverageTimeCounter mKeyCounter = new AverageTimeCounter();
+    final AverageTimeCounter mBeforeDeleteKeyCounter = new AverageTimeCounter();
+    final AverageTimeCounter mDuringRepeatedDeleteKeysCounter = new AverageTimeCounter();
+    final AverageTimeCounter mAfterDeleteKeyCounter = new AverageTimeCounter();
+
+    static class AverageTimeCounter {
+        int mCount;
+        int mTotalTime;
+
+        public void reset() {
+            mCount = 0;
+            mTotalTime = 0;
+        }
+
+        public void add(long deltaTime) {
+            mCount++;
+            mTotalTime += deltaTime;
+        }
+
+        public int getAverageTime() {
+            if (mCount == 0) {
+                return 0;
+            }
+            return mTotalTime / mCount;
+        }
+    }
+
+    // To account for the interruptions when the user's attention is directed elsewhere, times
+    // longer than MIN_TYPING_INTERMISSION are not counted when estimating this statistic.
+    public static final int MIN_TYPING_INTERMISSION = 5 * 1000;  // in milliseconds
+    public static final int MIN_DELETION_INTERMISSION = 15 * 1000;  // in milliseconds
+
+    // The last time that a tap was performed
+    private long mLastTapTime;
+    // The type of the last keypress (delete key or not)
+    boolean mIsLastKeyDeleteKey;
+
+    private static final Statistics sInstance = new Statistics();
+
+    public static Statistics getInstance() {
+        return sInstance;
+    }
+
+    private Statistics() {
+        reset();
+    }
+
+    public void reset() {
+        mCharCount = 0;
+        mLetterCount = 0;
+        mNumberCount = 0;
+        mSpaceCount = 0;
+        mDeleteKeyCount = 0;
+        mWordCount = 0;
+        mIsEmptyUponStarting = true;
+        mIsEmptinessStateKnown = false;
+        mKeyCounter.reset();
+        mBeforeDeleteKeyCounter.reset();
+        mDuringRepeatedDeleteKeysCounter.reset();
+        mAfterDeleteKeyCounter.reset();
+
+        mLastTapTime = 0;
+        mIsLastKeyDeleteKey = false;
+    }
+
+    public void recordChar(int codePoint, long time) {
+        final long delta = time - mLastTapTime;
+        if (codePoint == Keyboard.CODE_DELETE) {
+            mDeleteKeyCount++;
+            if (delta < MIN_DELETION_INTERMISSION) {
+                if (mIsLastKeyDeleteKey) {
+                    mDuringRepeatedDeleteKeysCounter.add(delta);
+                } else {
+                    mBeforeDeleteKeyCounter.add(delta);
+                }
+            }
+            mIsLastKeyDeleteKey = true;
+        } else {
+            mCharCount++;
+            if (Character.isDigit(codePoint)) {
+                mNumberCount++;
+            }
+            if (Character.isLetter(codePoint)) {
+                mLetterCount++;
+            }
+            if (Character.isSpaceChar(codePoint)) {
+                mSpaceCount++;
+            }
+            if (mIsLastKeyDeleteKey && delta < MIN_DELETION_INTERMISSION) {
+                mAfterDeleteKeyCounter.add(delta);
+            } else if (!mIsLastKeyDeleteKey && delta < MIN_TYPING_INTERMISSION) {
+                mKeyCounter.add(delta);
+            }
+            mIsLastKeyDeleteKey = false;
+        }
+        mLastTapTime = time;
+    }
+
+    public void recordWordEntered() {
+        mWordCount++;
+    }
+
+    public void setIsEmptyUponStarting(final boolean isEmpty) {
+        mIsEmptyUponStarting = isEmpty;
+        mIsEmptinessStateKnown = true;
+    }
+}