From 2996cf24efa4f61d61f2cc82e234d20c97041aa5 Mon Sep 17 00:00:00 2001 From: Kurt Partridge Date: Wed, 1 Aug 2012 17:26:45 -0700 Subject: ResearchLogger log RichInputConnection Move many ResearchLogger data collection points to RichInputConnection. By collecting data here, developers do not have to remember to keep the ResearchLog consistent with calls to the RichInputConnection. In addition, some unnecessary log points were removed, and the ResearchLogger is now independent of LatinImeLogger. multi-project change with I67bbf5fe8fd821da967a50b69e467caafc7fd388 Bug: 6188932 Change-Id: I49ccee1dcffe8d41401b42a637dd0da8dd11d90d --- .../inputmethod/research/ResearchLogger.java | 211 +++++++++++---------- 1 file changed, 113 insertions(+), 98 deletions(-) (limited to 'java/src/com/android/inputmethod/research/ResearchLogger.java') diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java index 68bd98a23..5bf7f7f4b 100644 --- a/java/src/com/android/inputmethod/research/ResearchLogger.java +++ b/java/src/com/android/inputmethod/research/ResearchLogger.java @@ -37,12 +37,14 @@ import android.os.IBinder; import android.text.TextUtils; import android.text.format.DateUtils; import android.util.Log; +import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.View.OnClickListener; import android.view.Window; import android.view.WindowManager; import android.view.inputmethod.CompletionInfo; +import android.view.inputmethod.CorrectionInfo; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.widget.Button; @@ -846,20 +848,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang stop(); } - private static final String[] EVENTKEYS_LATINIME_COMMITTEXT = { - "LatinIMECommitText", "typedWord" - }; - - public static void latinIME_commitText(final CharSequence typedWord) { - final String scrubbedWord = scrubDigitsFromString(typedWord.toString()); - final Object[] values = { - scrubbedWord - }; - final ResearchLogger researchLogger = getInstance(); - researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_COMMITTEXT, values); - researchLogger.onWordComplete(scrubbedWord); - } - private static final String[] EVENTKEYS_USER_FEEDBACK = { "UserFeedback", "FeedbackContents" }; @@ -914,47 +902,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_ONCODEINPUT, values); } - private static final String[] EVENTKEYS_CORRECTION = { - "LogCorrection", "subgroup", "before", "after", "position" - }; - public static void logCorrection(final String subgroup, final String before, final String after, - final int position) { - final Object[] values = { - subgroup, scrubDigitsFromString(before), scrubDigitsFromString(after), position - }; - getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_CORRECTION, values); - } - - private static final String[] EVENTKEYS_LATINIME_COMMITCURRENTAUTOCORRECTION = { - "LatinIMECommitCurrentAutoCorrection", "typedWord", "autoCorrection" - }; - public static void latinIME_commitCurrentAutoCorrection(final String typedWord, - final String autoCorrection) { - final Object[] values = { - scrubDigitsFromString(typedWord), scrubDigitsFromString(autoCorrection) - }; - final ResearchLogger researchLogger = getInstance(); - researchLogger.enqueuePotentiallyPrivateEvent( - EVENTKEYS_LATINIME_COMMITCURRENTAUTOCORRECTION, values); - } - - private static final String[] EVENTKEYS_LATINIME_DELETESURROUNDINGTEXT = { - "LatinIMEDeleteSurroundingText", "length" - }; - public static void latinIME_deleteSurroundingText(final int length) { - final Object[] values = { - length - }; - getInstance().enqueueEvent(EVENTKEYS_LATINIME_DELETESURROUNDINGTEXT, values); - } - - private static final String[] EVENTKEYS_LATINIME_DOUBLESPACEAUTOPERIOD = { - "LatinIMEDoubleSpaceAutoPeriod" - }; - public static void latinIME_doubleSpaceAutoPeriod() { - getInstance().enqueueEvent(EVENTKEYS_LATINIME_DOUBLESPACEAUTOPERIOD, EVENTKEYS_NULLVALUES); - } - private static final String[] EVENTKEYS_LATINIME_ONDISPLAYCOMPLETIONS = { "LatinIMEOnDisplayCompletions", "applicationSpecifiedCompletions" }; @@ -979,6 +926,10 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang public static void latinIME_onWindowHidden(final int savedSelectionStart, final int savedSelectionEnd, final InputConnection ic) { if (ic != null) { + // Capture the TextView contents. This will trigger onUpdateSelection(), so we + // set sLatinIMEExpectingUpdateSelection so that when onUpdateSelection() is called, + // it can tell that it was generated by the logging code, and not by the user, and + // therefore keep user-visible state as is. ic.beginBatchEdit(); ic.performContextMenuAction(android.R.id.selectAll); CharSequence charSequence = ic.getSelectedText(0); @@ -1048,29 +999,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_ONUPDATESELECTION, values); } - private static final String[] EVENTKEYS_LATINIME_PERFORMEDITORACTION = { - "LatinIMEPerformEditorAction", "imeActionNext" - }; - public static void latinIME_performEditorAction(final int imeActionNext) { - final Object[] values = { - imeActionNext - }; - getInstance().enqueueEvent(EVENTKEYS_LATINIME_PERFORMEDITORACTION, values); - } - - private static final String[] EVENTKEYS_LATINIME_PICKAPPLICATIONSPECIFIEDCOMPLETION = { - "LatinIMEPickApplicationSpecifiedCompletion", "index", "text", "x", "y" - }; - public static void latinIME_pickApplicationSpecifiedCompletion(final int index, - final CharSequence cs, int x, int y) { - final Object[] values = { - index, cs, x, y - }; - final ResearchLogger researchLogger = getInstance(); - researchLogger.enqueuePotentiallyPrivateEvent( - EVENTKEYS_LATINIME_PICKAPPLICATIONSPECIFIEDCOMPLETION, values); - } - private static final String[] EVENTKEYS_LATINIME_PICKSUGGESTIONMANUALLY = { "LatinIMEPickSuggestionManually", "replacedWord", "index", "suggestion", "x", "y" }; @@ -1096,21 +1024,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang getInstance().enqueueEvent(EVENTKEYS_LATINIME_PUNCTUATIONSUGGESTION, values); } - private static final String[] EVENTKEYS_LATINIME_REVERTDOUBLESPACEWHILEINBATCHEDIT = { - "LatinIMERevertDoubleSpaceWhileInBatchEdit" - }; - public static void latinIME_revertDoubleSpaceWhileInBatchEdit() { - getInstance().enqueueEvent(EVENTKEYS_LATINIME_REVERTDOUBLESPACEWHILEINBATCHEDIT, - EVENTKEYS_NULLVALUES); - } - - private static final String[] EVENTKEYS_LATINIME_REVERTSWAPPUNCTUATION = { - "LatinIMERevertSwapPunctuation" - }; - public static void latinIME_revertSwapPunctuation() { - getInstance().enqueueEvent(EVENTKEYS_LATINIME_REVERTSWAPPUNCTUATION, EVENTKEYS_NULLVALUES); - } - private static final String[] EVENTKEYS_LATINIME_SENDKEYCODEPOINT = { "LatinIMESendKeyCodePoint", "code" }; @@ -1121,12 +1034,11 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_SENDKEYCODEPOINT, values); } - private static final String[] EVENTKEYS_LATINIME_SWAPSWAPPERANDSPACEWHILEINBATCHEDIT = { - "LatinIMESwapSwapperAndSpaceWhileInBatchEdit" + private static final String[] EVENTKEYS_LATINIME_SWAPSWAPPERANDSPACE = { + "LatinIMESwapSwapperAndSpace" }; - public static void latinIME_swapSwapperAndSpaceWhileInBatchEdit() { - getInstance().enqueueEvent(EVENTKEYS_LATINIME_SWAPSWAPPERANDSPACEWHILEINBATCHEDIT, - EVENTKEYS_NULLVALUES); + public static void latinIME_swapSwapperAndSpace() { + getInstance().enqueueEvent(EVENTKEYS_LATINIME_SWAPSWAPPERANDSPACE, EVENTKEYS_NULLVALUES); } private static final String[] EVENTKEYS_MAINKEYBOARDVIEW_ONLONGPRESS = { @@ -1245,6 +1157,109 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_POINTERTRACKER_ONMOVEEVENT, values); } + private static final String[] EVENTKEYS_RICHINPUTCONNECTION_COMMITCOMPLETION = { + "RichInputConnectionCommitCompletion", "completionInfo" + }; + public static void richInputConnection_commitCompletion(final CompletionInfo completionInfo) { + final Object[] values = { + completionInfo + }; + final ResearchLogger researchLogger = getInstance(); + researchLogger.enqueuePotentiallyPrivateEvent( + EVENTKEYS_RICHINPUTCONNECTION_COMMITCOMPLETION, values); + } + + private static final String[] EVENTKEYS_RICHINPUTCONNECTION_COMMITCORRECTION = { + "RichInputConnectionCommitCorrection", "CorrectionInfo" + }; + public static void richInputConnection_commitCorrection(CorrectionInfo correctionInfo) { + final String typedWord = correctionInfo.getOldText().toString(); + final String autoCorrection = correctionInfo.getNewText().toString(); + final Object[] values = { + scrubDigitsFromString(typedWord), scrubDigitsFromString(autoCorrection) + }; + final ResearchLogger researchLogger = getInstance(); + researchLogger.enqueuePotentiallyPrivateEvent( + EVENTKEYS_RICHINPUTCONNECTION_COMMITCORRECTION, values); + } + + private static final String[] EVENTKEYS_RICHINPUTCONNECTION_COMMITTEXT = { + "RichInputConnectionCommitText", "typedWord", "newCursorPosition" + }; + public static void richInputConnection_commitText(final CharSequence typedWord, + final int newCursorPosition) { + final String scrubbedWord = scrubDigitsFromString(typedWord.toString()); + final Object[] values = { + scrubbedWord, newCursorPosition + }; + final ResearchLogger researchLogger = getInstance(); + researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_RICHINPUTCONNECTION_COMMITTEXT, + values); + researchLogger.onWordComplete(scrubbedWord); + } + + private static final String[] EVENTKEYS_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT = { + "RichInputConnectionDeleteSurroundingText", "beforeLength", "afterLength" + }; + public static void richInputConnection_deleteSurroundingText(final int beforeLength, + final int afterLength) { + final Object[] values = { + beforeLength, afterLength + }; + getInstance().enqueueEvent(EVENTKEYS_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT, values); + } + + private static final String[] EVENTKEYS_RICHINPUTCONNECTION_FINISHCOMPOSINGTEXT = { + "RichInputConnectionFinishComposingText" + }; + public static void richInputConnection_finishComposingText() { + getInstance().enqueueEvent(EVENTKEYS_RICHINPUTCONNECTION_FINISHCOMPOSINGTEXT, + EVENTKEYS_NULLVALUES); + } + + private static final String[] EVENTKEYS_RICHINPUTCONNECTION_PERFORMEDITORACTION = { + "RichInputConnectionPerformEditorAction", "imeActionNext" + }; + public static void richInputConnection_performEditorAction(final int imeActionNext) { + final Object[] values = { + imeActionNext + }; + getInstance().enqueueEvent(EVENTKEYS_RICHINPUTCONNECTION_PERFORMEDITORACTION, values); + } + + private static final String[] EVENTKEYS_RICHINPUTCONNECTION_SENDKEYEVENT = { + "RichInputConnectionSendKeyEvent", "eventTime", "action", "code" + }; + public static void richInputConnection_sendKeyEvent(final KeyEvent keyEvent) { + final Object[] values = { + keyEvent.getEventTime(), + keyEvent.getAction(), + keyEvent.getKeyCode() + }; + getInstance().enqueueEvent(EVENTKEYS_RICHINPUTCONNECTION_SENDKEYEVENT, values); + } + + private static final String[] EVENTKEYS_RICHINPUTCONNECTION_SETCOMPOSINGTEXT = { + "RichInputConnectionSetComposingText", "text", "newCursorPosition" + }; + public static void richInputConnection_setComposingText(final CharSequence text, + final int newCursorPosition) { + final Object[] values = { + text, newCursorPosition + }; + getInstance().enqueueEvent(EVENTKEYS_RICHINPUTCONNECTION_SETCOMPOSINGTEXT, values); + } + + private static final String[] EVENTKEYS_RICHINPUTCONNECTION_SETSELECTION = { + "RichInputConnectionSetSelection", "from", "to" + }; + public static void richInputConnection_setSelection(final int from, final int to) { + final Object[] values = { + from, to + }; + getInstance().enqueueEvent(EVENTKEYS_RICHINPUTCONNECTION_SETSELECTION, values); + } + private static final String[] EVENTKEYS_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT = { "SuddenJumpingTouchEventHandlerOnTouchEvent", "motionEvent" }; -- cgit v1.2.3-83-g751a From 5d6cdef255922e524a4c9cdaa249d4a194f3720f Mon Sep 17 00:00:00 2001 From: Kurt Partridge Date: Mon, 30 Jul 2012 21:55:42 -0700 Subject: ResearchLogger to track simple statistics Bug: 6188932 Change-Id: I1ecb5e1fd08f17af998b87414771fa7d90ef5f81 --- .../inputmethod/research/ResearchLogger.java | 57 +++++++- .../android/inputmethod/research/Statistics.java | 146 +++++++++++++++++++++ 2 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 java/src/com/android/inputmethod/research/Statistics.java (limited to 'java/src/com/android/inputmethod/research/ResearchLogger.java') diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java index 5bf7f7f4b..07b8c5995 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.graphics.Paint.Style; 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 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang 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 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang 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 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } /* package */ void stop() { + logStatistics(); + publishLogUnit(mCurrentLogUnit, true); + mCurrentLogUnit = new LogUnit(); + if (mMainResearchLog != null) { mMainResearchLog.stop(); } @@ -305,6 +337,26 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } } + 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 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang mLoggingFrequencyState.onWordLogged(); } mCurrentLogUnit = new LogUnit(); + mStatistics.recordWordEntered(); } private void publishLogUnit(LogUnit logUnit, boolean isPrivacySensitive) { @@ -899,7 +952,9 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang 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 000000000..4a2cd079c --- /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; + } +} -- cgit v1.2.3-83-g751a From 838e6c1e5b734ed2d7b68a082f61b723d1aa35a2 Mon Sep 17 00:00:00 2001 From: Kurt Partridge Date: Thu, 2 Aug 2012 20:22:29 -0700 Subject: ResearchLogging capture full n-gram data - Captures complete motion data for all words in an n-gram. - Also filters n-grams properly; if any word in the n-gram is not in the dictionary, it is not included. - Simplify ResearchLog to not require explicit state - Added LogBuffer class MainLogBuffer class to allow n-gram-level decisions about privacy. - Moved LogUnit out from ResearchLogger multi-project change with Id997fb6572daa3ff55591afa1676953bb204f33a Because of a strange Gerrit interaction, earlier patchsets were recorded as the following separate changes (to soon be abandoned): - I59d86ae28123890ddb7aabc7dc9a73a75eecbca4 - I9cc95266fa4500d019e09ec53ec00e5dea7ffd38 - Ie6f23a2035e160f231406df1388a43725380df1d - Id87909e96fa7fa91174c86395a964404f3490fe1 - I57c5cbde0425c42538d589fa4d1802b3e5688f7c - Ic1f9d7727e416da1b6c4f1a45b53750cd4088dcf - I20a72ccc4ba1bc08ed3ee39bff71d2adb376588c - I417f6f364144b90085f5fef4bd2de1a40b5dfa79 Bug: 6188932 Change-Id: I771e57e3b8c0eb6721c2022e775cb0b528bdff93 --- java/res/values/strings.xml | 3 +- .../com/android/inputmethod/latin/LatinIME.java | 8 +- .../inputmethod/research/FeedbackActivity.java | 8 +- .../android/inputmethod/research/LogBuffer.java | 111 +++++ .../com/android/inputmethod/research/LogUnit.java | 81 ++++ .../inputmethod/research/MainLogBuffer.java | 127 ++++++ .../android/inputmethod/research/ResearchLog.java | 257 +++++------- .../inputmethod/research/ResearchLogUploader.java | 37 +- .../inputmethod/research/ResearchLogger.java | 465 ++++++++------------- .../android/inputmethod/research/Statistics.java | 4 +- 10 files changed, 619 insertions(+), 482 deletions(-) create mode 100644 java/src/com/android/inputmethod/research/LogBuffer.java create mode 100644 java/src/com/android/inputmethod/research/LogUnit.java create mode 100644 java/src/com/android/inputmethod/research/MainLogBuffer.java (limited to 'java/src/com/android/inputmethod/research/ResearchLogger.java') diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml index 07b3f31c7..f2468f5a4 100644 --- a/java/res/values/strings.xml +++ b/java/res/values/strings.xml @@ -261,7 +261,8 @@ Send feedback - Include last 5 words entered + + Include last %d words entered Enter your feedback here. diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index f3614399a..60f1a204b 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -1231,11 +1231,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } mLastKeyTime = when; mConnection.beginBatchEdit(); - - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_onCodeInput(primaryCode, x, y); - } - final KeyboardSwitcher switcher = mKeyboardSwitcher; // The space state depends only on the last character pressed and its own previous // state. Here, we revert the space state to neutral if the key is actually modifying @@ -1317,6 +1312,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mLastComposedWord.deactivate(); mEnteredText = null; mConnection.endBatchEdit(); + if (ProductionFlag.IS_EXPERIMENTAL) { + ResearchLogger.latinIME_onCodeInput(primaryCode, x, y); + } } // Called from PointerTracker through the KeyboardActionListener interface diff --git a/java/src/com/android/inputmethod/research/FeedbackActivity.java b/java/src/com/android/inputmethod/research/FeedbackActivity.java index c9f3b476a..11eae8813 100644 --- a/java/src/com/android/inputmethod/research/FeedbackActivity.java +++ b/java/src/com/android/inputmethod/research/FeedbackActivity.java @@ -18,10 +18,7 @@ package com.android.inputmethod.research; import android.app.Activity; import android.os.Bundle; -import android.text.Editable; -import android.view.View; import android.widget.CheckBox; -import android.widget.EditText; import com.android.inputmethod.latin.R; @@ -31,6 +28,11 @@ public class FeedbackActivity extends Activity { super.onCreate(savedInstanceState); setContentView(R.layout.research_feedback_activity); final FeedbackLayout layout = (FeedbackLayout) findViewById(R.id.research_feedback_layout); + final CheckBox checkbox = (CheckBox) findViewById(R.id.research_feedback_include_history); + final CharSequence cs = checkbox.getText(); + final String actualString = String.format(cs.toString(), + ResearchLogger.FEEDBACK_WORD_BUFFER_SIZE); + checkbox.setText(actualString); layout.setActivity(this); } diff --git a/java/src/com/android/inputmethod/research/LogBuffer.java b/java/src/com/android/inputmethod/research/LogBuffer.java new file mode 100644 index 000000000..65f5f83ae --- /dev/null +++ b/java/src/com/android/inputmethod/research/LogBuffer.java @@ -0,0 +1,111 @@ +/* + * 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 java.util.LinkedList; + +/** + * A buffer that holds a fixed number of LogUnits. + * + * LogUnits are added in and shifted out in temporal order. Only a subset of the LogUnits are + * actual words; the other LogUnits do not count toward the word limit. Once the buffer reaches + * capacity, adding another LogUnit that is a word evicts the oldest LogUnits out one at a time to + * stay under the capacity limit. + */ +public class LogBuffer { + protected final LinkedList mLogUnits; + /* package for test */ int mWordCapacity; + // The number of members of mLogUnits that are actual words. + protected int mNumActualWords; + + /** + * Create a new LogBuffer that can hold a fixed number of LogUnits that are words (and + * unlimited number of non-word LogUnits), and that outputs its result to a researchLog. + * + * @param wordCapacity maximum number of words + */ + LogBuffer(final int wordCapacity) { + if (wordCapacity <= 0) { + throw new IllegalArgumentException("wordCapacity must be 1 or greater."); + } + mLogUnits = new LinkedList(); + mWordCapacity = wordCapacity; + mNumActualWords = 0; + } + + /** + * Adds a new LogUnit to the front of the LIFO queue, evicting existing LogUnit's + * (oldest first) if word capacity is reached. + */ + public void shiftIn(LogUnit newLogUnit) { + if (newLogUnit.getWord() == null) { + // This LogUnit isn't a word, so it doesn't count toward the word-limit. + mLogUnits.add(newLogUnit); + return; + } + if (mNumActualWords == mWordCapacity) { + shiftOutThroughFirstWord(); + } + mLogUnits.add(newLogUnit); + mNumActualWords++; // Must be a word, or we wouldn't be here. + } + + private void shiftOutThroughFirstWord() { + while (!mLogUnits.isEmpty()) { + final LogUnit logUnit = mLogUnits.removeFirst(); + onShiftOut(logUnit); + if (logUnit.hasWord()) { + // Successfully shifted out a word-containing LogUnit and made space for the new + // LogUnit. + mNumActualWords--; + break; + } + } + } + + /** + * Removes all LogUnits from the buffer without calling onShiftOut(). + */ + public void clear() { + mLogUnits.clear(); + mNumActualWords = 0; + } + + /** + * Called when a LogUnit is removed from the LogBuffer as a result of a shiftIn. LogUnits are + * removed in the order entered. This method is not called when shiftOut is called directly. + * + * Base class does nothing; subclasses may override. + */ + protected void onShiftOut(LogUnit logUnit) { + } + + /** + * Called to deliberately remove the oldest LogUnit. Usually called when draining the + * LogBuffer. + */ + public LogUnit shiftOut() { + if (mLogUnits.isEmpty()) { + return null; + } + final LogUnit logUnit = mLogUnits.removeFirst(); + if (logUnit.hasWord()) { + mNumActualWords--; + } + return logUnit; + } +} diff --git a/java/src/com/android/inputmethod/research/LogUnit.java b/java/src/com/android/inputmethod/research/LogUnit.java new file mode 100644 index 000000000..8a80664f5 --- /dev/null +++ b/java/src/com/android/inputmethod/research/LogUnit.java @@ -0,0 +1,81 @@ +/* + * 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 java.util.ArrayList; + +/** + * A group of log statements related to each other. + * + * A LogUnit is collection of LogStatements, each of which is generated by at a particular point + * in the code. (There is no LogStatement class; the data is stored across the instance variables + * here.) A single LogUnit's statements can correspond to all the calls made while in the same + * composing region, or all the calls between committing the last composing region, and the first + * character of the next composing region. + * + * Individual statements in a log may be marked as potentially private. If so, then they are only + * published to a ResearchLog if the ResearchLogger determines that publishing the entire LogUnit + * will not violate the user's privacy. Checks for this may include whether other LogUnits have + * been published recently, or whether the LogUnit contains numbers, etc. + */ +/* package */ class LogUnit { + private final ArrayList mKeysList = new ArrayList(); + private final ArrayList mValuesList = new ArrayList(); + private final ArrayList mIsPotentiallyPrivate = new ArrayList(); + private String mWord; + private boolean mContainsDigit; + + public void addLogStatement(final String[] keys, final Object[] values, + final Boolean isPotentiallyPrivate) { + mKeysList.add(keys); + mValuesList.add(values); + mIsPotentiallyPrivate.add(isPotentiallyPrivate); + } + + public void publishTo(final ResearchLog researchLog, final boolean isIncludingPrivateData) { + final int size = mKeysList.size(); + for (int i = 0; i < size; i++) { + if (!mIsPotentiallyPrivate.get(i) || isIncludingPrivateData) { + researchLog.outputEvent(mKeysList.get(i), mValuesList.get(i)); + } + } + } + + public void setWord(String word) { + mWord = word; + } + + public String getWord() { + return mWord; + } + + public boolean hasWord() { + return mWord != null; + } + + public void setContainsDigit() { + mContainsDigit = true; + } + + public boolean hasDigit() { + return mContainsDigit; + } + + public boolean isEmpty() { + return mKeysList.isEmpty(); + } +} diff --git a/java/src/com/android/inputmethod/research/MainLogBuffer.java b/java/src/com/android/inputmethod/research/MainLogBuffer.java new file mode 100644 index 000000000..745768d35 --- /dev/null +++ b/java/src/com/android/inputmethod/research/MainLogBuffer.java @@ -0,0 +1,127 @@ +/* + * 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.latin.Dictionary; +import com.android.inputmethod.latin.Suggest; + +import java.util.Random; + +public class MainLogBuffer extends LogBuffer { + // The size of the n-grams logged. E.g. N_GRAM_SIZE = 2 means to sample bigrams. + private static final int N_GRAM_SIZE = 2; + // The number of words between n-grams to omit from the log. + private static final int DEFAULT_NUMBER_OF_WORDS_BETWEEN_SAMPLES = 18; + + private final ResearchLog mResearchLog; + private Suggest mSuggest; + + // The minimum periodicity with which n-grams can be sampled. E.g. mWinWordPeriod is 10 if + // every 10th bigram is sampled, i.e., words 1-8 are not, but the bigram at words 9 and 10, etc. + // for 11-18, and the bigram at words 19 and 20. If an n-gram is not safe (e.g. it contains a + // number in the middle or an out-of-vocabulary word), then sampling is delayed until a safe + // n-gram does appear. + /* package for test */ int mMinWordPeriod; + + // Counter for words left to suppress before an n-gram can be sampled. Reset to mMinWordPeriod + // after a sample is taken. + /* package for test */ int mWordsUntilSafeToSample; + + public MainLogBuffer(final ResearchLog researchLog) { + super(N_GRAM_SIZE); + mResearchLog = researchLog; + mMinWordPeriod = DEFAULT_NUMBER_OF_WORDS_BETWEEN_SAMPLES + N_GRAM_SIZE; + final Random random = new Random(); + mWordsUntilSafeToSample = random.nextInt(mMinWordPeriod); + } + + public void setSuggest(Suggest suggest) { + mSuggest = suggest; + } + + @Override + public void shiftIn(final LogUnit newLogUnit) { + super.shiftIn(newLogUnit); + if (newLogUnit.hasWord()) { + if (mWordsUntilSafeToSample > 0) { + mWordsUntilSafeToSample--; + } + } + } + + public void resetWordCounter() { + mWordsUntilSafeToSample = mMinWordPeriod; + } + + /** + * Determines whether the content of the MainLogBuffer can be safely uploaded in its complete + * form and still protect the user's privacy. + * + * The size of the MainLogBuffer is just enough to hold one n-gram, its corrections, and any + * non-character data that is typed between words. The decision about privacy is made based on + * the buffer's entire content. If it is decided that the privacy risks are too great to upload + * the contents of this buffer, a censored version of the LogItems may still be uploaded. E.g., + * the screen orientation and other characteristics about the device can be uploaded without + * revealing much about the user. + */ + public boolean isSafeToLog() { + // Check that we are not sampling too frequently. Having sampled recently might disclose + // too much of the user's intended meaning. + if (mWordsUntilSafeToSample > 0) { + return false; + } + if (mSuggest == null || !mSuggest.hasMainDictionary()) { + // Main dictionary is unavailable. Since we cannot check it, we cannot tell if a word + // is out-of-vocabulary or not. Therefore, we must judge the entire buffer contents to + // potentially pose a privacy risk. + return false; + } + // Reload the dictionary in case it has changed (e.g., because the user has changed + // languages). + final Dictionary dictionary = mSuggest.getMainDictionary(); + if (dictionary == null) { + return false; + } + // Check each word in the buffer. If any word poses a privacy threat, we cannot upload the + // complete buffer contents in detail. + final int length = mLogUnits.size(); + for (int i = 0; i < length; i++) { + final LogUnit logUnit = mLogUnits.get(i); + final String word = logUnit.getWord(); + if (word == null) { + // Digits outside words are a privacy threat. + if (logUnit.hasDigit()) { + return false; + } + } else { + // Words not in the dictionary are a privacy threat. + if (!(dictionary.isValidWord(word))) { + return false; + } + } + } + // All checks have passed; this buffer's content can be safely uploaded. + return true; + } + + @Override + protected void onShiftOut(LogUnit logUnit) { + if (mResearchLog != null) { + mResearchLog.publish(logUnit, false /* isIncludingPrivateData */); + } + } +} diff --git a/java/src/com/android/inputmethod/research/ResearchLog.java b/java/src/com/android/inputmethod/research/ResearchLog.java index 18bf3c07f..71a6d6a78 100644 --- a/java/src/com/android/inputmethod/research/ResearchLog.java +++ b/java/src/com/android/inputmethod/research/ResearchLog.java @@ -26,7 +26,6 @@ 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; -import com.android.inputmethod.research.ResearchLogger.LogUnit; import java.io.BufferedWriter; import java.io.File; @@ -37,6 +36,7 @@ import java.io.OutputStreamWriter; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.Executors; +import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -51,21 +51,22 @@ import java.util.concurrent.TimeUnit; */ public class ResearchLog { private static final String TAG = ResearchLog.class.getSimpleName(); - private static final JsonWriter NULL_JSON_WRITER = new JsonWriter( - new OutputStreamWriter(new NullOutputStream())); + private static final boolean DEBUG = false; + private static final long FLUSH_DELAY_IN_MS = 1000 * 5; + private static final int ABORT_TIMEOUT_IN_MS = 1000 * 4; - final ScheduledExecutorService mExecutor; + /* package */ final ScheduledExecutorService mExecutor; /* package */ final File mFile; private JsonWriter mJsonWriter = NULL_JSON_WRITER; + // true if at least one byte of data has been written out to the log file. This must be + // remembered because JsonWriter requires that calls matching calls to beginObject and + // endObject, as well as beginArray and endArray, and the file is opened lazily, only when + // it is certain that data will be written. Alternatively, the matching call exceptions + // could be caught, but this might suppress other errors. + private boolean mHasWrittenData = false; - private int mLoggingState; - private static final int LOGGING_STATE_UNSTARTED = 0; - private static final int LOGGING_STATE_READY = 1; // don't create file until necessary - private static final int LOGGING_STATE_RUNNING = 2; - private static final int LOGGING_STATE_STOPPING = 3; - private static final int LOGGING_STATE_STOPPED = 4; - private static final long FLUSH_DELAY_IN_MS = 1000 * 5; - + private static final JsonWriter NULL_JSON_WRITER = new JsonWriter( + new OutputStreamWriter(new NullOutputStream())); private static class NullOutputStream extends OutputStream { /** {@inheritDoc} */ @Override @@ -84,128 +85,81 @@ public class ResearchLog { } } - public ResearchLog(File outputFile) { - mExecutor = Executors.newSingleThreadScheduledExecutor(); + public ResearchLog(final File outputFile) { if (outputFile == null) { throw new IllegalArgumentException(); } + mExecutor = Executors.newSingleThreadScheduledExecutor(); mFile = outputFile; - mLoggingState = LOGGING_STATE_UNSTARTED; - } - - public synchronized void start() throws IOException { - switch (mLoggingState) { - case LOGGING_STATE_UNSTARTED: - mLoggingState = LOGGING_STATE_READY; - break; - case LOGGING_STATE_READY: - case LOGGING_STATE_RUNNING: - case LOGGING_STATE_STOPPING: - case LOGGING_STATE_STOPPED: - break; - } } - public synchronized void stop() { - switch (mLoggingState) { - case LOGGING_STATE_UNSTARTED: - mLoggingState = LOGGING_STATE_STOPPED; - break; - case LOGGING_STATE_READY: - case LOGGING_STATE_RUNNING: - mExecutor.submit(new Callable() { - @Override - public Object call() throws Exception { - try { - mJsonWriter.endArray(); - mJsonWriter.flush(); - mJsonWriter.close(); - } finally { - boolean success = mFile.setWritable(false, false); - mLoggingState = LOGGING_STATE_STOPPED; - } - return null; + public synchronized void close() { + mExecutor.submit(new Callable() { + @Override + public Object call() throws Exception { + try { + if (mHasWrittenData) { + mJsonWriter.endArray(); + mJsonWriter.flush(); + mJsonWriter.close(); + mHasWrittenData = false; } - }); - removeAnyScheduledFlush(); - mExecutor.shutdown(); - mLoggingState = LOGGING_STATE_STOPPING; - break; - case LOGGING_STATE_STOPPING: - case LOGGING_STATE_STOPPED: - } + } catch (Exception e) { + Log.d(TAG, "error when closing ResearchLog:"); + e.printStackTrace(); + } finally { + if (mFile.exists()) { + mFile.setWritable(false, false); + } + } + return null; + } + }); + removeAnyScheduledFlush(); + mExecutor.shutdown(); } - public boolean isAlive() { - switch (mLoggingState) { - case LOGGING_STATE_UNSTARTED: - case LOGGING_STATE_READY: - case LOGGING_STATE_RUNNING: - return true; - } - return false; - } + private boolean mIsAbortSuccessful; - public void waitUntilStopped(final int timeoutInMs) throws InterruptedException { + public synchronized void abort() { + mExecutor.submit(new Callable() { + @Override + public Object call() throws Exception { + try { + if (mHasWrittenData) { + mJsonWriter.endArray(); + mJsonWriter.close(); + mHasWrittenData = false; + } + } finally { + mIsAbortSuccessful = mFile.delete(); + } + return null; + } + }); removeAnyScheduledFlush(); mExecutor.shutdown(); - mExecutor.awaitTermination(timeoutInMs, TimeUnit.MILLISECONDS); } - public synchronized void abort() { - switch (mLoggingState) { - case LOGGING_STATE_UNSTARTED: - mLoggingState = LOGGING_STATE_STOPPED; - isAbortSuccessful = true; - break; - case LOGGING_STATE_READY: - case LOGGING_STATE_RUNNING: - mExecutor.submit(new Callable() { - @Override - public Object call() throws Exception { - try { - mJsonWriter.endArray(); - mJsonWriter.close(); - } finally { - isAbortSuccessful = mFile.delete(); - } - return null; - } - }); - removeAnyScheduledFlush(); - mExecutor.shutdown(); - mLoggingState = LOGGING_STATE_STOPPING; - break; - case LOGGING_STATE_STOPPING: - case LOGGING_STATE_STOPPED: - } + public boolean blockingAbort() throws InterruptedException { + abort(); + mExecutor.awaitTermination(ABORT_TIMEOUT_IN_MS, TimeUnit.MILLISECONDS); + return mIsAbortSuccessful; } - private boolean isAbortSuccessful; - public boolean isAbortSuccessful() { - return isAbortSuccessful; + public void awaitTermination(int delay, TimeUnit timeUnit) throws InterruptedException { + mExecutor.awaitTermination(delay, timeUnit); } /* package */ synchronized void flush() { - switch (mLoggingState) { - case LOGGING_STATE_UNSTARTED: - break; - case LOGGING_STATE_READY: - case LOGGING_STATE_RUNNING: - removeAnyScheduledFlush(); - mExecutor.submit(mFlushCallable); - break; - case LOGGING_STATE_STOPPING: - case LOGGING_STATE_STOPPED: - } + removeAnyScheduledFlush(); + mExecutor.submit(mFlushCallable); } - private Callable mFlushCallable = new Callable() { + private final Callable mFlushCallable = new Callable() { @Override public Object call() throws Exception { - if (mLoggingState == LOGGING_STATE_RUNNING) { - mJsonWriter.flush(); - } + mJsonWriter.flush(); return null; } }; @@ -224,56 +178,40 @@ public class ResearchLog { mFlushFuture = mExecutor.schedule(mFlushCallable, FLUSH_DELAY_IN_MS, TimeUnit.MILLISECONDS); } - public synchronized void publishPublicEvents(final LogUnit logUnit) { - switch (mLoggingState) { - case LOGGING_STATE_UNSTARTED: - break; - case LOGGING_STATE_READY: - case LOGGING_STATE_RUNNING: - mExecutor.submit(new Callable() { - @Override - public Object call() throws Exception { - logUnit.publishPublicEventsTo(ResearchLog.this); - scheduleFlush(); - return null; - } - }); - break; - case LOGGING_STATE_STOPPING: - case LOGGING_STATE_STOPPED: - } - } - - public synchronized void publishAllEvents(final LogUnit logUnit) { - switch (mLoggingState) { - case LOGGING_STATE_UNSTARTED: - break; - case LOGGING_STATE_READY: - case LOGGING_STATE_RUNNING: - mExecutor.submit(new Callable() { - @Override - public Object call() throws Exception { - logUnit.publishAllEventsTo(ResearchLog.this); - scheduleFlush(); - return null; - } - }); - break; - case LOGGING_STATE_STOPPING: - case LOGGING_STATE_STOPPED: + public synchronized void publish(final LogUnit logUnit, final boolean isIncludingPrivateData) { + try { + mExecutor.submit(new Callable() { + @Override + public Object call() throws Exception { + logUnit.publishTo(ResearchLog.this, isIncludingPrivateData); + scheduleFlush(); + return null; + } + }); + } catch (RejectedExecutionException e) { + // TODO: Add code to record loss of data, and report. } } private static final String CURRENT_TIME_KEY = "_ct"; private static final String UPTIME_KEY = "_ut"; private static final String EVENT_TYPE_KEY = "_ty"; + void outputEvent(final String[] keys, final Object[] values) { - // not thread safe. + // Not thread safe. + if (keys.length == 0) { + return; + } + if (DEBUG) { + if (keys.length != values.length + 1) { + Log.d(TAG, "Key and Value list sizes do not match. " + keys[0]); + } + } try { if (mJsonWriter == NULL_JSON_WRITER) { mJsonWriter = new JsonWriter(new BufferedWriter(new FileWriter(mFile))); - mJsonWriter.setLenient(true); mJsonWriter.beginArray(); + mHasWrittenData = true; } mJsonWriter.beginObject(); mJsonWriter.name(CURRENT_TIME_KEY).value(System.currentTimeMillis()); @@ -283,8 +221,8 @@ public class ResearchLog { for (int i = 0; i < length; i++) { mJsonWriter.name(keys[i + 1]); Object value = values[i]; - if (value instanceof String) { - mJsonWriter.value((String) value); + if (value instanceof CharSequence) { + mJsonWriter.value(value.toString()); } else if (value instanceof Number) { mJsonWriter.value((Number) value); } else if (value instanceof Boolean) { @@ -331,14 +269,11 @@ public class ResearchLog { SuggestedWords words = (SuggestedWords) value; mJsonWriter.beginObject(); mJsonWriter.name("typedWordValid").value(words.mTypedWordValid); - mJsonWriter.name("willAutoCorrect") - .value(words.mWillAutoCorrect); + mJsonWriter.name("willAutoCorrect").value(words.mWillAutoCorrect); mJsonWriter.name("isPunctuationSuggestions") - .value(words.mIsPunctuationSuggestions); - mJsonWriter.name("isObsoleteSuggestions") - .value(words.mIsObsoleteSuggestions); - mJsonWriter.name("isPrediction") - .value(words.mIsPrediction); + .value(words.mIsPunctuationSuggestions); + mJsonWriter.name("isObsoleteSuggestions").value(words.mIsObsoleteSuggestions); + mJsonWriter.name("isPrediction").value(words.mIsPrediction); mJsonWriter.name("words"); mJsonWriter.beginArray(); final int size = words.size(); @@ -363,8 +298,8 @@ public class ResearchLog { try { mJsonWriter.close(); } catch (IllegalStateException e1) { - // assume that this is just the json not being terminated properly. - // ignore + // Assume that this is just the json not being terminated properly. + // Ignore } catch (IOException e1) { e1.printStackTrace(); } finally { diff --git a/java/src/com/android/inputmethod/research/ResearchLogUploader.java b/java/src/com/android/inputmethod/research/ResearchLogUploader.java index 3b1213009..9904a1de2 100644 --- a/java/src/com/android/inputmethod/research/ResearchLogUploader.java +++ b/java/src/com/android/inputmethod/research/ResearchLogUploader.java @@ -27,7 +27,6 @@ import android.os.BatteryManager; import android.util.Log; import com.android.inputmethod.latin.R; -import com.android.inputmethod.latin.R.string; import java.io.BufferedReader; import java.io.File; @@ -48,6 +47,7 @@ public final class ResearchLogUploader { private static final String TAG = ResearchLogUploader.class.getSimpleName(); private static final int UPLOAD_INTERVAL_IN_MS = 1000 * 60 * 15; // every 15 min private static final int BUF_SIZE = 1024 * 8; + protected static final int TIMEOUT_IN_MS = 1000 * 4; private final boolean mCanUpload; private final Context mContext; @@ -55,8 +55,6 @@ public final class ResearchLogUploader { private final URL mUrl; private final ScheduledExecutorService mExecutor; - private Runnable doUploadRunnable = new UploadRunnable(null, false); - public ResearchLogUploader(final Context context, final File filesDir) { mContext = context; mFilesDir = filesDir; @@ -93,11 +91,15 @@ public final class ResearchLogUploader { public void start() { if (mCanUpload) { - Log.d(TAG, "scheduling regular uploading"); - mExecutor.scheduleWithFixedDelay(doUploadRunnable, UPLOAD_INTERVAL_IN_MS, - UPLOAD_INTERVAL_IN_MS, TimeUnit.MILLISECONDS); - } else { - Log.d(TAG, "no permission to upload"); + mExecutor.scheduleWithFixedDelay(new UploadRunnable(null /* logToWaitFor */, + null /* callback */, false /* forceUpload */), + UPLOAD_INTERVAL_IN_MS, UPLOAD_INTERVAL_IN_MS, TimeUnit.MILLISECONDS); + } + } + + public void uploadAfterCompletion(final ResearchLog researchLog, final Callback callback) { + if (mCanUpload) { + mExecutor.submit(new UploadRunnable(researchLog, callback, true /* forceUpload */)); } } @@ -106,7 +108,8 @@ public final class ResearchLogUploader { // another upload happening right now, as it may have missed the latest changes. // TODO: Reschedule regular upload tests starting from now. if (mCanUpload) { - mExecutor.submit(new UploadRunnable(callback, true)); + mExecutor.submit(new UploadRunnable(null /* logToWaitFor */, callback, + true /* forceUpload */)); } } @@ -130,19 +133,33 @@ public final class ResearchLogUploader { } class UploadRunnable implements Runnable { + private final ResearchLog mLogToWaitFor; private final Callback mCallback; private final boolean mForceUpload; - public UploadRunnable(final Callback callback, final boolean forceUpload) { + public UploadRunnable(final ResearchLog logToWaitFor, final Callback callback, + final boolean forceUpload) { + mLogToWaitFor = logToWaitFor; mCallback = callback; mForceUpload = forceUpload; } @Override public void run() { + if (mLogToWaitFor != null) { + waitFor(mLogToWaitFor); + } doUpload(); } + private void waitFor(final ResearchLog researchLog) { + try { + researchLog.awaitTermination(TIMEOUT_IN_MS, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + private void doUpload() { if (!mForceUpload && (!isExternallyPowered() || !hasWifiConnection())) { return; diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java index 07b8c5995..77c498648 100644 --- a/java/src/com/android/inputmethod/research/ResearchLogger.java +++ b/java/src/com/android/inputmethod/research/ResearchLogger.java @@ -67,11 +67,8 @@ import com.android.inputmethod.latin.SuggestedWords; import com.android.inputmethod.latin.define.ProductionFlag; import java.io.File; -import java.io.IOException; import java.text.SimpleDateFormat; -import java.util.ArrayList; import java.util.Date; -import java.util.List; import java.util.Locale; import java.util.UUID; @@ -97,24 +94,21 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang new SimpleDateFormat("yyyyMMddHHmmssS", Locale.US); private static final boolean IS_SHOWING_INDICATOR = true; private static final boolean IS_SHOWING_INDICATOR_CLEARLY = false; + public static final int FEEDBACK_WORD_BUFFER_SIZE = 5; // constants related to specific log points 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 int ABORT_TIMEOUT_IN_MS = 10 * 1000; // timeout to notify user private static final ResearchLogger sInstance = new ResearchLogger(); // to write to a different filename, e.g., for testing, set mFile before calling start() /* package */ File mFilesDir; /* package */ String mUUIDString; /* package */ ResearchLog mMainResearchLog; - // The mIntentionalResearchLog records all events for the session, private or not (excepting - // passwords). It is written to permanent storage only if the user explicitly commands - // the system to do so. - /* package */ ResearchLog mIntentionalResearchLog; - // LogUnits are queued here and released only when the user requests the intentional log. - private List mIntentionalResearchLogQueue = new ArrayList(); + /* package */ ResearchLog mFeedbackLog; + /* package */ MainLogBuffer mMainLogBuffer; + /* package */ LogBuffer mFeedbackLogBuffer; private boolean mIsPasswordView = false; private boolean mIsLoggingSuspended = false; @@ -139,9 +133,10 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang private KeyboardSwitcher mKeyboardSwitcher; private InputMethodService mInputMethodService; private final Statistics mStatistics; - private ResearchLogUploader mResearchLogUploader; + private LogUnit mCurrentLogUnit = new LogUnit(); + private ResearchLogger() { mStatistics = Statistics.getInstance(); } @@ -264,6 +259,16 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang e.apply(); } + private void setLoggingAllowed(boolean enableLogging) { + if (mPrefs == null) { + return; + } + Editor e = mPrefs.edit(); + e.putBoolean(PREF_USABILITY_STUDY_MODE, enableLogging); + e.apply(); + sIsLogging = enableLogging; + } + private File createLogFile(File filesDir) { final StringBuilder sb = new StringBuilder(); sb.append(FILENAME_PREFIX).append('-'); @@ -310,97 +315,58 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang Log.w(TAG, "IME storage directory does not exist. Cannot start logging."); return; } - try { - if (mMainResearchLog == null || !mMainResearchLog.isAlive()) { - mMainResearchLog = new ResearchLog(createLogFile(mFilesDir)); - } - mMainResearchLog.start(); - if (mIntentionalResearchLog == null || !mIntentionalResearchLog.isAlive()) { - mIntentionalResearchLog = new ResearchLog(createLogFile(mFilesDir)); - } - mIntentionalResearchLog.start(); - } catch (IOException e) { - Log.w(TAG, "Could not start ResearchLogger."); + if (mMainLogBuffer == null) { + mMainResearchLog = new ResearchLog(createLogFile(mFilesDir)); + mMainLogBuffer = new MainLogBuffer(mMainResearchLog); + mMainLogBuffer.setSuggest(mSuggest); + } + if (mFeedbackLogBuffer == null) { + mFeedbackLog = new ResearchLog(createLogFile(mFilesDir)); + // LogBuffer is one more than FEEDBACK_WORD_BUFFER_SIZE, because it must also hold + // the feedback LogUnit itself. + mFeedbackLogBuffer = new LogBuffer(FEEDBACK_WORD_BUFFER_SIZE + 1); } } /* package */ void stop() { logStatistics(); - publishLogUnit(mCurrentLogUnit, true); - mCurrentLogUnit = new LogUnit(); + commitCurrentLogUnit(); - if (mMainResearchLog != null) { - mMainResearchLog.stop(); - } - if (mIntentionalResearchLog != null) { - mIntentionalResearchLog.stop(); + if (mMainLogBuffer != null) { + publishLogBuffer(mMainLogBuffer, mMainResearchLog, false /* isIncludingPrivateData */); + mMainResearchLog.close(); + mMainLogBuffer = null; } - } - - 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; + if (mFeedbackLogBuffer != null) { + mFeedbackLog.close(); + mFeedbackLogBuffer = null; } - Editor e = mPrefs.edit(); - e.putBoolean(PREF_USABILITY_STUDY_MODE, enableLogging); - e.apply(); - sIsLogging = enableLogging; } public boolean abort() { boolean didAbortMainLog = false; - if (mMainResearchLog != null) { - mMainResearchLog.abort(); + if (mMainLogBuffer != null) { + mMainLogBuffer.clear(); try { - mMainResearchLog.waitUntilStopped(ABORT_TIMEOUT_IN_MS); + didAbortMainLog = mMainResearchLog.blockingAbort(); } catch (InterruptedException e) { - // interrupted early. carry on. - } - if (mMainResearchLog.isAbortSuccessful()) { - didAbortMainLog = true; + // Don't know whether this succeeded or not. We assume not; this is reported + // to the caller. } - mMainResearchLog = null; + mMainLogBuffer = null; } - boolean didAbortIntentionalLog = false; - if (mIntentionalResearchLog != null) { - mIntentionalResearchLog.abort(); + boolean didAbortFeedbackLog = false; + if (mFeedbackLogBuffer != null) { + mFeedbackLogBuffer.clear(); try { - mIntentionalResearchLog.waitUntilStopped(ABORT_TIMEOUT_IN_MS); + didAbortFeedbackLog = mFeedbackLog.blockingAbort(); } catch (InterruptedException e) { - // interrupted early. carry on. + // Don't know whether this succeeded or not. We assume not; this is reported + // to the caller. } - if (mIntentionalResearchLog.isAbortSuccessful()) { - didAbortIntentionalLog = true; - } - mIntentionalResearchLog = null; - } - return didAbortMainLog && didAbortIntentionalLog; - } - - /* package */ void flush() { - if (mMainResearchLog != null) { - mMainResearchLog.flush(); + mFeedbackLogBuffer = null; } + return didAbortMainLog && didAbortFeedbackLog; } private void restart() { @@ -504,79 +470,39 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang latinIME.launchKeyboardedDialogActivity(FeedbackActivity.class); } - private ResearchLog mFeedbackLog; - private List mFeedbackQueue; - private ResearchLog mSavedMainResearchLog; - private ResearchLog mSavedIntentionalResearchLog; - private List mSavedIntentionalResearchLogQueue; - - private void saveLogsForFeedback() { - mFeedbackLog = mIntentionalResearchLog; - if (mIntentionalResearchLogQueue != null) { - mFeedbackQueue = new ArrayList(mIntentionalResearchLogQueue); - } else { - mFeedbackQueue = null; - } - mSavedMainResearchLog = mMainResearchLog; - mSavedIntentionalResearchLog = mIntentionalResearchLog; - mSavedIntentionalResearchLogQueue = mIntentionalResearchLogQueue; - - mMainResearchLog = null; - mIntentionalResearchLog = null; - mIntentionalResearchLogQueue = new ArrayList(); - } - - private static final int LOG_DRAIN_TIMEOUT_IN_MS = 1000 * 5; + private static final String[] EVENTKEYS_FEEDBACK = { + "UserTimestamp", "contents" + }; public void sendFeedback(final String feedbackContents, final boolean includeHistory) { - if (includeHistory && mFeedbackLog != null) { - try { - LogUnit headerLogUnit = new LogUnit(); - headerLogUnit.addLogAtom(EVENTKEYS_INTENTIONAL_LOG, EVENTKEYS_NULLVALUES, false); - mFeedbackLog.publishAllEvents(headerLogUnit); - for (LogUnit logUnit : mFeedbackQueue) { - mFeedbackLog.publishAllEvents(logUnit); - } - userFeedback(mFeedbackLog, feedbackContents); - mFeedbackLog.stop(); - try { - mFeedbackLog.waitUntilStopped(LOG_DRAIN_TIMEOUT_IN_MS); - } catch (InterruptedException e) { - e.printStackTrace(); - } - mIntentionalResearchLog = new ResearchLog(createLogFile(mFilesDir)); - mIntentionalResearchLog.start(); - } catch (IOException e) { - e.printStackTrace(); - } finally { - mIntentionalResearchLogQueue.clear(); - } - mResearchLogUploader.uploadNow(null); - } else { - // create a separate ResearchLog just for feedback - final ResearchLog feedbackLog = new ResearchLog(createLogFile(mFilesDir)); - try { - feedbackLog.start(); - userFeedback(feedbackLog, feedbackContents); - feedbackLog.stop(); - feedbackLog.waitUntilStopped(LOG_DRAIN_TIMEOUT_IN_MS); - mResearchLogUploader.uploadNow(null); - } catch (IOException e) { - e.printStackTrace(); - } catch (InterruptedException e) { - e.printStackTrace(); - } + if (mFeedbackLogBuffer == null) { + return; + } + if (!includeHistory) { + mFeedbackLogBuffer.clear(); } + commitCurrentLogUnit(); + final LogUnit feedbackLogUnit = new LogUnit(); + final Object[] values = { + feedbackContents + }; + feedbackLogUnit.addLogStatement(EVENTKEYS_FEEDBACK, values, + false /* isPotentiallyPrivate */); + mFeedbackLogBuffer.shiftIn(feedbackLogUnit); + publishLogBuffer(mFeedbackLogBuffer, mFeedbackLog, true /* isIncludingPrivateData */); + mFeedbackLog.close(); + mResearchLogUploader.uploadAfterCompletion(mFeedbackLog, null); + mFeedbackLog = new ResearchLog(createLogFile(mFilesDir)); } public void onLeavingSendFeedbackDialog() { mInFeedbackDialog = false; - mMainResearchLog = mSavedMainResearchLog; - mIntentionalResearchLog = mSavedIntentionalResearchLog; - mIntentionalResearchLogQueue = mSavedIntentionalResearchLogQueue; } public void initSuggest(Suggest suggest) { mSuggest = suggest; + if (mMainLogBuffer != null) { + mMainLogBuffer.setSuggest(mSuggest); + } } private void setIsPasswordView(boolean isPasswordView) { @@ -584,7 +510,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } private boolean isAllowedToLog() { - return !mIsPasswordView && !mIsLoggingSuspended && sIsLogging; + return !mIsPasswordView && !mIsLoggingSuspended && sIsLogging && !mInFeedbackDialog; } public void requestIndicatorRedraw() { @@ -631,13 +557,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } } - 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 Object[] EVENTKEYS_NULLVALUES = {}; - private LogUnit mCurrentLogUnit = new LogUnit(); - /** * Buffer a research log event, flagging it as privacy-sensitive. * @@ -653,10 +574,14 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang final Object[] values) { assert values.length + 1 == keys.length; if (isAllowedToLog()) { - mCurrentLogUnit.addLogAtom(keys, values, true); + mCurrentLogUnit.addLogStatement(keys, values, true /* isPotentiallyPrivate */); } } + private void setCurrentLogUnitContainsDigitFlag() { + mCurrentLogUnit.setContainsDigit(); + } + /** * Buffer a research log event, flaggint it as not privacy-sensitive. * @@ -672,140 +597,54 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang private synchronized void enqueueEvent(final String[] keys, final Object[] values) { assert values.length + 1 == keys.length; if (isAllowedToLog()) { - mCurrentLogUnit.addLogAtom(keys, values, false); + mCurrentLogUnit.addLogStatement(keys, values, false /* isPotentiallyPrivate */); } } - // Used to track how often words are logged. Too-frequent logging can leak - // semantics, disclosing private data. - /* package for test */ static class LoggingFrequencyState { - private static final int DEFAULT_WORD_LOG_FREQUENCY = 10; - private int mWordsRemainingToSkip; - private final int mFrequency; - - /** - * Tracks how often words may be uploaded. - * - * @param frequency 1=Every word, 2=Every other word, etc. - */ - public LoggingFrequencyState(int frequency) { - mFrequency = frequency; - mWordsRemainingToSkip = mFrequency; - } - - public void onWordLogged() { - mWordsRemainingToSkip = mFrequency; - } - - public void onWordNotLogged() { - if (mWordsRemainingToSkip > 1) { - mWordsRemainingToSkip--; + /* package for test */ void commitCurrentLogUnit() { + if (!mCurrentLogUnit.isEmpty()) { + if (mMainLogBuffer != null) { + mMainLogBuffer.shiftIn(mCurrentLogUnit); + if (mMainLogBuffer.isSafeToLog() && mMainResearchLog != null) { + publishLogBuffer(mMainLogBuffer, mMainResearchLog, + true /* isIncludingPrivateData */); + mMainLogBuffer.resetWordCounter(); + } + } + if (mFeedbackLogBuffer != null) { + mFeedbackLogBuffer.shiftIn(mCurrentLogUnit); } + mCurrentLogUnit = new LogUnit(); + Log.d(TAG, "commitCurrentLogUnit"); } + } - public boolean isSafeToLog() { - return mWordsRemainingToSkip <= 1; + /* package for test */ void publishLogBuffer(final LogBuffer logBuffer, + final ResearchLog researchLog, final boolean isIncludingPrivateData) { + LogUnit logUnit; + while ((logUnit = logBuffer.shiftOut()) != null) { + researchLog.publish(logUnit, isIncludingPrivateData); } } - /* package for test */ LoggingFrequencyState mLoggingFrequencyState = - new LoggingFrequencyState(LoggingFrequencyState.DEFAULT_WORD_LOG_FREQUENCY); - - /* package for test */ boolean isPrivacyThreat(String word) { - // Current checks: - // - Word not in dictionary - // - Word contains numbers - // - Privacy-safe word not logged recently - if (TextUtils.isEmpty(word)) { - return false; - } - if (!mLoggingFrequencyState.isSafeToLog()) { - return true; - } + private boolean hasOnlyLetters(final String word) { final int length = word.length(); - boolean hasLetter = false; for (int i = 0; i < length; i = word.offsetByCodePoints(i, 1)) { - final int codePoint = Character.codePointAt(word, i); - if (Character.isDigit(codePoint)) { - return true; - } - if (Character.isLetter(codePoint)) { - hasLetter = true; - break; // Word may contain digits, but will only be allowed if in the dictionary. - } - } - if (hasLetter) { - if (mDictionary == null && mSuggest != null && mSuggest.hasMainDictionary()) { - mDictionary = mSuggest.getMainDictionary(); + final int codePoint = word.codePointAt(i); + if (!Character.isLetter(codePoint)) { + return false; } - if (mDictionary == null) { - // Can't access dictionary. Assume privacy threat. - return true; - } - return !(mDictionary.isValidWord(word)); - } - // No letters, no numbers. Punctuation, space, or something else. - return false; - } - - private void onWordComplete(String word) { - if (isPrivacyThreat(word)) { - publishLogUnit(mCurrentLogUnit, true); - mLoggingFrequencyState.onWordNotLogged(); - } else { - publishLogUnit(mCurrentLogUnit, false); - mLoggingFrequencyState.onWordLogged(); } - mCurrentLogUnit = new LogUnit(); - mStatistics.recordWordEntered(); + return true; } - private void publishLogUnit(LogUnit logUnit, boolean isPrivacySensitive) { - if (!isAllowedToLog()) { - return; - } - if (mMainResearchLog == null) { - return; - } - if (isPrivacySensitive) { - mMainResearchLog.publishPublicEvents(logUnit); - } else { - mMainResearchLog.publishAllEvents(logUnit); - } - mIntentionalResearchLogQueue.add(logUnit); - } - - /* package */ void publishCurrentLogUnit(ResearchLog researchLog, boolean isPrivacySensitive) { - publishLogUnit(mCurrentLogUnit, isPrivacySensitive); - } - - static class LogUnit { - private final List mKeysList = new ArrayList(); - private final List mValuesList = new ArrayList(); - private final List mIsPotentiallyPrivate = new ArrayList(); - - private void addLogAtom(final String[] keys, final Object[] values, - final Boolean isPotentiallyPrivate) { - mKeysList.add(keys); - mValuesList.add(values); - mIsPotentiallyPrivate.add(isPotentiallyPrivate); - } - - public void publishPublicEventsTo(ResearchLog researchLog) { - final int size = mKeysList.size(); - for (int i = 0; i < size; i++) { - if (!mIsPotentiallyPrivate.get(i)) { - researchLog.outputEvent(mKeysList.get(i), mValuesList.get(i)); - } - } - } - - public void publishAllEventsTo(ResearchLog researchLog) { - final int size = mKeysList.size(); - for (int i = 0; i < size; i++) { - researchLog.outputEvent(mKeysList.get(i), mValuesList.get(i)); - } + private void onWordComplete(final String word) { + Log.d(TAG, "onWordComplete: " + word); + if (word != null && word.length() > 0 && hasOnlyLetters(word)) { + mCurrentLogUnit.setWord(word); + mStatistics.recordWordEntered(); } + commitCurrentLogUnit(); } private static int scrubDigitFromCodePoint(int codePoint) { @@ -858,12 +697,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang return WORD_REPLACEMENT_STRING; } - // Special methods related to startup, shutdown, logging itself - - private static final String[] EVENTKEYS_INTENTIONAL_LOG = { - "IntentionalLog" - }; - private static final String[] EVENTKEYS_LATINIME_ONSTARTINPUTVIEWINTERNAL = { "LatinIMEOnStartInputViewInternal", "uuid", "packageName", "inputType", "imeOptions", "fieldId", "display", "model", "prefs", "versionCode", "versionName", "outputFormatVersion" @@ -871,9 +704,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang public static void latinIME_onStartInputViewInternal(final EditorInfo editorInfo, final SharedPreferences prefs) { final ResearchLogger researchLogger = getInstance(); - if (researchLogger.mInFeedbackDialog) { - researchLogger.saveLogsForFeedback(); - } researchLogger.start(); if (editorInfo != null) { final Context context = researchLogger.mInputMethodService; @@ -905,16 +735,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang "UserFeedback", "FeedbackContents" }; - private void userFeedback(ResearchLog researchLog, String feedbackContents) { - // this method is special; it directs the feedbackContents to a particular researchLog - final LogUnit logUnit = new LogUnit(); - final Object[] values = { - feedbackContents - }; - logUnit.addLogAtom(EVENTKEYS_USER_FEEDBACK, values, false); - researchLog.publishAllEvents(logUnit); - } - // Regular logging methods private static final String[] EVENTKEYS_MAINKEYBOARDVIEW_PROCESSMOTIONEVENT = { @@ -949,12 +769,16 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang "LatinIMEOnCodeInput", "code", "x", "y" }; public static void latinIME_onCodeInput(final int code, final int x, final int y) { + final long time = SystemClock.uptimeMillis(); + final ResearchLogger researchLogger = getInstance(); final Object[] values = { Keyboard.printableCode(scrubDigitFromCodePoint(code)), x, y }; - final ResearchLogger researchLogger = getInstance(); researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_ONCODEINPUT, values); - researchLogger.mStatistics.recordChar(code, SystemClock.uptimeMillis()); + if (Character.isDigit(code)) { + researchLogger.setCurrentLogUnitContainsDigitFlag(); + } + researchLogger.mStatistics.recordChar(code, time); } private static final String[] EVENTKEYS_LATINIME_ONDISPLAYCOMPLETIONS = { @@ -1019,9 +843,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } final ResearchLogger researchLogger = getInstance(); researchLogger.enqueueEvent(EVENTKEYS_LATINIME_ONWINDOWHIDDEN, values); - // Play it safe. Remove privacy-sensitive events. - researchLogger.publishLogUnit(researchLogger.mCurrentLogUnit, true); - researchLogger.mCurrentLogUnit = new LogUnit(); + researchLogger.commitCurrentLogUnit(); getInstance().stop(); } } @@ -1086,7 +908,11 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang final Object[] values = { Keyboard.printableCode(scrubDigitFromCodePoint(code)) }; - getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_SENDKEYCODEPOINT, values); + final ResearchLogger researchLogger = getInstance(); + researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_SENDKEYCODEPOINT, values); + if (Character.isDigit(code)) { + researchLogger.setCurrentLogUnitContainsDigitFlag(); + } } private static final String[] EVENTKEYS_LATINIME_SWAPSWAPPERANDSPACE = { @@ -1224,10 +1050,21 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang EVENTKEYS_RICHINPUTCONNECTION_COMMITCOMPLETION, values); } + // Disabled for privacy-protection reasons. Because this event comes after + // richInputConnection_commitText, which is the event used to separate LogUnits, the + // data in this event can be associated with the next LogUnit, revealing information + // about the current word even if it was supposed to be suppressed. The occurrance of + // autocorrection can be determined by examining the difference between the text strings in + // the last call to richInputConnection_setComposingText before + // richInputConnection_commitText, so it's not a data loss. + // TODO: Figure out how to log this event without loss of privacy. + /* private static final String[] EVENTKEYS_RICHINPUTCONNECTION_COMMITCORRECTION = { - "RichInputConnectionCommitCorrection", "CorrectionInfo" + "RichInputConnectionCommitCorrection", "typedWord", "autoCorrection" }; + */ public static void richInputConnection_commitCorrection(CorrectionInfo correctionInfo) { + /* final String typedWord = correctionInfo.getOldText().toString(); final String autoCorrection = correctionInfo.getNewText().toString(); final Object[] values = { @@ -1236,6 +1073,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang final ResearchLogger researchLogger = getInstance(); researchLogger.enqueuePotentiallyPrivateEvent( EVENTKEYS_RICHINPUTCONNECTION_COMMITCORRECTION, values); + */ } private static final String[] EVENTKEYS_RICHINPUTCONNECTION_COMMITTEXT = { @@ -1261,7 +1099,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang final Object[] values = { beforeLength, afterLength }; - getInstance().enqueueEvent(EVENTKEYS_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT, values); + getInstance().enqueuePotentiallyPrivateEvent( + EVENTKEYS_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT, values); } private static final String[] EVENTKEYS_RICHINPUTCONNECTION_FINISHCOMPOSINGTEXT = { @@ -1291,7 +1130,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang keyEvent.getAction(), keyEvent.getKeyCode() }; - getInstance().enqueueEvent(EVENTKEYS_RICHINPUTCONNECTION_SENDKEYEVENT, values); + getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_RICHINPUTCONNECTION_SENDKEYEVENT, + values); } private static final String[] EVENTKEYS_RICHINPUTCONNECTION_SETCOMPOSINGTEXT = { @@ -1299,10 +1139,14 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang }; public static void richInputConnection_setComposingText(final CharSequence text, final int newCursorPosition) { + if (text == null) { + throw new RuntimeException("setComposingText is null"); + } final Object[] values = { text, newCursorPosition }; - getInstance().enqueueEvent(EVENTKEYS_RICHINPUTCONNECTION_SETCOMPOSINGTEXT, values); + getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_RICHINPUTCONNECTION_SETCOMPOSINGTEXT, + values); } private static final String[] EVENTKEYS_RICHINPUTCONNECTION_SETSELECTION = { @@ -1312,7 +1156,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang final Object[] values = { from, to }; - getInstance().enqueueEvent(EVENTKEYS_RICHINPUTCONNECTION_SETSELECTION, values); + getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_RICHINPUTCONNECTION_SETSELECTION, + values); } private static final String[] EVENTKEYS_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT = { @@ -1347,4 +1192,24 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang public void userTimestamp() { getInstance().enqueueEvent(EVENTKEYS_USER_TIMESTAMP, EVENTKEYS_NULLVALUES); } + + 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); + } } diff --git a/java/src/com/android/inputmethod/research/Statistics.java b/java/src/com/android/inputmethod/research/Statistics.java index 4a2cd079c..eab465aa2 100644 --- a/java/src/com/android/inputmethod/research/Statistics.java +++ b/java/src/com/android/inputmethod/research/Statistics.java @@ -66,8 +66,8 @@ public class Statistics { // 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 + public static final int MIN_TYPING_INTERMISSION = 2 * 1000; // in milliseconds + public static final int MIN_DELETION_INTERMISSION = 10 * 1000; // in milliseconds // The last time that a tap was performed private long mLastTapTime; -- cgit v1.2.3-83-g751a