aboutsummaryrefslogtreecommitdiffstats
path: root/java/src
diff options
context:
space:
mode:
Diffstat (limited to 'java/src')
-rw-r--r--java/src/com/android/inputmethod/latin/LatinIME.java84
-rw-r--r--java/src/com/android/inputmethod/latin/RichInputConnection.java86
-rw-r--r--java/src/com/android/inputmethod/research/FeedbackActivity.java8
-rw-r--r--java/src/com/android/inputmethod/research/LogBuffer.java111
-rw-r--r--java/src/com/android/inputmethod/research/LogUnit.java81
-rw-r--r--java/src/com/android/inputmethod/research/MainLogBuffer.java127
-rw-r--r--java/src/com/android/inputmethod/research/ResearchLog.java257
-rw-r--r--java/src/com/android/inputmethod/research/ResearchLogUploader.java37
-rw-r--r--java/src/com/android/inputmethod/research/ResearchLogger.java673
-rw-r--r--java/src/com/android/inputmethod/research/Statistics.java146
10 files changed, 976 insertions, 634 deletions
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 455086015..ff7654a14 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -898,13 +898,13 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
}
}
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_onDisplayCompletions(applicationSpecifiedCompletions);
- }
if (!mCurrentSettings.isApplicationSpecifiedCompletionsOn()) return;
mApplicationSpecifiedCompletions = applicationSpecifiedCompletions;
if (applicationSpecifiedCompletions == null) {
clearSuggestionStrip();
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinIME_onDisplayCompletions(null);
+ }
return;
}
@@ -926,6 +926,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// this case? This says to keep whatever the user typed.
mWordComposer.setAutoCorrection(mWordComposer.getTypedWord());
setSuggestionStripShown(true);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinIME_onDisplayCompletions(applicationSpecifiedCompletions);
+ }
}
private void setSuggestionStripShownInternal(boolean shown, boolean needsInputViewShown) {
@@ -1046,9 +1049,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
final CharSequence typedWord = mWordComposer.getTypedWord();
if (typedWord.length() > 0) {
mConnection.commitText(typedWord, 1);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_commitText(typedWord);
- }
final CharSequence prevWord = addToUserHistoryDictionary(typedWord);
mLastComposedWord = mWordComposer.commitWord(
LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD, typedWord.toString(),
@@ -1090,12 +1090,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
if (lastTwo != null && lastTwo.length() == 2
&& lastTwo.charAt(0) == Keyboard.CODE_SPACE) {
mConnection.deleteSurroundingText(2, 0);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_deleteSurroundingText(2);
- }
mConnection.commitText(lastTwo.charAt(1) + " ", 1);
if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_swapSwapperAndSpaceWhileInBatchEdit();
+ ResearchLogger.latinIME_swapSwapperAndSpace();
}
mKeyboardSwitcher.updateShiftState();
}
@@ -1112,9 +1109,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
mHandler.cancelDoubleSpacesTimer();
mConnection.deleteSurroundingText(2, 0);
mConnection.commitText(". ", 1);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_doubleSpaceAutoPeriod();
- }
mKeyboardSwitcher.updateShiftState();
return true;
}
@@ -1178,9 +1172,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
private void performEditorAction(int actionId) {
mConnection.performEditorAction(actionId);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_performEditorAction(actionId);
- }
}
private void handleLanguageSwitchKey() {
@@ -1217,6 +1208,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}.
if (code >= '0' && code <= '9') {
super.sendKeyChar((char)code);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinIME_sendKeyCodePoint(code);
+ }
return;
}
@@ -1233,9 +1227,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
final String text = new String(new int[] { code }, 0, 1);
mConnection.commitText(text, text.length());
}
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_sendKeyCodePoint(code);
- }
}
// Implementation of {@link KeyboardActionListener}.
@@ -1247,11 +1238,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
@@ -1333,6 +1319,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
@@ -1346,9 +1335,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
sendKeyCodePoint(Keyboard.CODE_SPACE);
}
mConnection.commitText(text, 1);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_commitText(text);
- }
mConnection.endBatchEdit();
mKeyboardSwitcher.updateShiftState();
mKeyboardSwitcher.onCodeInput(Keyboard.CODE_OUTPUT_TEXT);
@@ -1447,9 +1433,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// like the smiley key or the .com key.
final int length = mEnteredText.length();
mConnection.deleteSurroundingText(length, 0);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_deleteSurroundingText(length);
- }
// If we have mEnteredText, then we know that mHasUncommittedTypedChars == false.
// In addition we know that spaceState is false, and that we should not be
// reverting any autocorrect at this point. So we can safely return.
@@ -1469,9 +1452,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
mHandler.postUpdateSuggestionStrip();
} else {
mConnection.deleteSurroundingText(1, 0);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_deleteSurroundingText(1);
- }
}
} else {
if (mLastComposedWord.canRevertCommit()) {
@@ -1500,9 +1480,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
final int lengthToDelete = mLastSelectionEnd - mLastSelectionStart;
mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd);
mConnection.deleteSurroundingText(lengthToDelete, 0);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_deleteSurroundingText(lengthToDelete);
- }
} else {
// There is no selection, just delete one character.
if (NOT_A_CURSOR_POSITION == mLastSelectionEnd) {
@@ -1521,14 +1498,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
} else {
mConnection.deleteSurroundingText(1, 0);
}
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_deleteSurroundingText(1);
- }
if (mDeleteCount > DELETE_ACCELERATE_AT) {
mConnection.deleteSurroundingText(1, 0);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_deleteSurroundingText(1);
- }
}
}
if (mCurrentSettings.isSuggestionsRequested(mDisplayOrientation)) {
@@ -1853,10 +1824,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
+ "is empty? Impossible! I must commit suicide.");
}
Utils.Stats.onAutoCorrection(typedWord, autoCorrection.toString(), separatorCodePoint);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_commitCurrentAutoCorrection(typedWord,
- autoCorrection.toString());
- }
mExpectingUpdateSelection = true;
commitChosenWord(autoCorrection, LastComposedWord.COMMIT_TYPE_DECIDED_WORD,
separatorCodePoint);
@@ -1882,13 +1849,13 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// So, LatinImeLogger logs "" as a user's input.
LatinImeLogger.logOnManualSuggestion("", suggestion.toString(), index, suggestedWords);
// Rely on onCodeInput to do the complicated swapping/stripping logic consistently.
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_punctuationSuggestion(index, suggestion, x, y);
- }
final int primaryCode = suggestion.charAt(0);
onCodeInput(primaryCode,
KeyboardActionListener.SUGGESTION_STRIP_COORDINATE,
KeyboardActionListener.SUGGESTION_STRIP_COORDINATE);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinIME_punctuationSuggestion(index, suggestion, x, y);
+ }
return;
}
@@ -1915,10 +1882,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
final CompletionInfo completionInfo = mApplicationSpecifiedCompletions[index];
mConnection.commitCompletion(completionInfo);
mConnection.endBatchEdit();
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_pickApplicationSpecifiedCompletion(index,
- completionInfo.getText(), x, y);
- }
return;
}
@@ -1927,12 +1890,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
final String replacedWord = mWordComposer.getTypedWord().toString();
LatinImeLogger.logOnManualSuggestion(replacedWord,
suggestion.toString(), index, suggestedWords);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_pickSuggestionManually(replacedWord, index, suggestion, x, y);
- }
mExpectingUpdateSelection = true;
commitChosenWord(suggestion, LastComposedWord.COMMIT_TYPE_MANUAL_PICK,
LastComposedWord.NOT_A_SEPARATOR);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinIME_pickSuggestionManually(replacedWord, index, suggestion, x, y);
+ }
mConnection.endBatchEdit();
// Don't allow cancellation of manual pick
mLastComposedWord.deactivate();
@@ -1967,9 +1930,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
final SuggestedWords suggestedWords = mSuggestionStripView.getSuggestions();
mConnection.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan(
this, chosenWord, suggestedWords, mIsMainDictionaryAvailable), 1);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_commitText(chosenWord);
- }
// Add the word to the user history dictionary
final CharSequence prevWord = addToUserHistoryDictionary(chosenWord);
// TODO: figure out here if this is an auto-correct or if the best word is actually
@@ -2036,9 +1996,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
mWordComposer.setComposingWord(word, mKeyboardSwitcher.getKeyboard());
final int length = word.length();
mConnection.deleteSurroundingText(length, 0);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_deleteSurroundingText(length);
- }
mConnection.setComposingText(word, 1);
mHandler.postUpdateSuggestionStrip();
}
@@ -2066,9 +2023,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
}
mConnection.deleteSurroundingText(deleteLength, 0);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_deleteSurroundingText(deleteLength);
- }
if (!TextUtils.isEmpty(previousWord) && !TextUtils.isEmpty(committedWord)) {
mUserHistoryDictionary.cancelAddingUserHistory(
previousWord.toString(), committedWord.toString());
diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java
index 8b4c17322..41e59e92d 100644
--- a/java/src/com/android/inputmethod/latin/RichInputConnection.java
+++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java
@@ -55,7 +55,9 @@ public class RichInputConnection {
public void beginBatchEdit() {
if (++mNestLevel == 1) {
mIC = mParent.getCurrentInputConnection();
- if (null != mIC) mIC.beginBatchEdit();
+ if (null != mIC) {
+ mIC.beginBatchEdit();
+ }
} else {
if (DBG) {
throw new RuntimeException("Nest level too deep");
@@ -66,7 +68,9 @@ public class RichInputConnection {
}
public void endBatchEdit() {
if (mNestLevel <= 0) Log.e(TAG, "Batch edit not in progress!"); // TODO: exception instead
- if (--mNestLevel == 0 && null != mIC) mIC.endBatchEdit();
+ if (--mNestLevel == 0 && null != mIC) {
+ mIC.endBatchEdit();
+ }
}
private void checkBatchEdit() {
@@ -79,12 +83,22 @@ public class RichInputConnection {
public void finishComposingText() {
checkBatchEdit();
- if (null != mIC) mIC.finishComposingText();
+ if (null != mIC) {
+ mIC.finishComposingText();
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.richInputConnection_finishComposingText();
+ }
+ }
}
public void commitText(final CharSequence text, final int i) {
checkBatchEdit();
- if (null != mIC) mIC.commitText(text, i);
+ if (null != mIC) {
+ mIC.commitText(text, i);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.richInputConnection_commitText(text, i);
+ }
+ }
}
public int getCursorCapsMode(final int inputType) {
@@ -107,37 +121,72 @@ public class RichInputConnection {
public void deleteSurroundingText(final int i, final int j) {
checkBatchEdit();
- if (null != mIC) mIC.deleteSurroundingText(i, j);
+ if (null != mIC) {
+ mIC.deleteSurroundingText(i, j);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.richInputConnection_deleteSurroundingText(i, j);
+ }
+ }
}
public void performEditorAction(final int actionId) {
mIC = mParent.getCurrentInputConnection();
- if (null != mIC) mIC.performEditorAction(actionId);
+ if (null != mIC) {
+ mIC.performEditorAction(actionId);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.richInputConnection_performEditorAction(actionId);
+ }
+ }
}
public void sendKeyEvent(final KeyEvent keyEvent) {
checkBatchEdit();
- if (null != mIC) mIC.sendKeyEvent(keyEvent);
+ if (null != mIC) {
+ mIC.sendKeyEvent(keyEvent);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.richInputConnection_sendKeyEvent(keyEvent);
+ }
+ }
}
public void setComposingText(final CharSequence text, final int i) {
checkBatchEdit();
- if (null != mIC) mIC.setComposingText(text, i);
+ if (null != mIC) {
+ mIC.setComposingText(text, i);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.richInputConnection_setComposingText(text, i);
+ }
+ }
}
public void setSelection(final int from, final int to) {
checkBatchEdit();
- if (null != mIC) mIC.setSelection(from, to);
+ if (null != mIC) {
+ mIC.setSelection(from, to);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.richInputConnection_setSelection(from, to);
+ }
+ }
}
public void commitCorrection(final CorrectionInfo correctionInfo) {
checkBatchEdit();
- if (null != mIC) mIC.commitCorrection(correctionInfo);
+ if (null != mIC) {
+ mIC.commitCorrection(correctionInfo);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.richInputConnection_commitCorrection(correctionInfo);
+ }
+ }
}
public void commitCompletion(final CompletionInfo completionInfo) {
checkBatchEdit();
- if (null != mIC) mIC.commitCompletion(completionInfo);
+ if (null != mIC) {
+ mIC.commitCompletion(completionInfo);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.richInputConnection_commitCompletion(completionInfo);
+ }
+ }
}
public CharSequence getNthPreviousWord(final String sentenceSeperators, final int n) {
@@ -315,9 +364,6 @@ public class RichInputConnection {
if (lastOne != null && lastOne.length() == 1
&& lastOne.charAt(0) == Keyboard.CODE_SPACE) {
deleteSurroundingText(1, 0);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_deleteSurroundingText(1);
- }
}
}
@@ -382,13 +428,7 @@ public class RichInputConnection {
return false;
}
deleteSurroundingText(2, 0);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_deleteSurroundingText(2);
- }
commitText(" ", 1);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_revertDoubleSpaceWhileInBatchEdit();
- }
return true;
}
@@ -409,13 +449,7 @@ public class RichInputConnection {
return false;
}
deleteSurroundingText(2, 0);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_deleteSurroundingText(2);
- }
commitText(" " + textBeforeCursor.subSequence(0, 1), 1);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_revertSwapPunctuation();
- }
return true;
}
}
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<LogUnit> 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<LogUnit>();
+ 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<String[]> mKeysList = new ArrayList<String[]>();
+ private final ArrayList<Object[]> mValuesList = new ArrayList<Object[]>();
+ private final ArrayList<Boolean> mIsPotentiallyPrivate = new ArrayList<Boolean>();
+ 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<Object>() {
- @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<Object>() {
+ @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<Object>() {
+ @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<Object>() {
- @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<Object> mFlushCallable = new Callable<Object>() {
+ private final Callable<Object> mFlushCallable = new Callable<Object>() {
@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<Object>() {
- @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<Object>() {
- @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<Object>() {
+ @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 cf6f31a0a..3cad2d099 100644
--- a/java/src/com/android/inputmethod/research/ResearchLogger.java
+++ b/java/src/com/android/inputmethod/research/ResearchLogger.java
@@ -34,15 +34,18 @@ 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;
+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;
@@ -64,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;
@@ -94,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<LogUnit> mIntentionalResearchLogQueue = new ArrayList<LogUnit>();
+ /* package */ ResearchLog mFeedbackLog;
+ /* package */ MainLogBuffer mMainLogBuffer;
+ /* package */ LogBuffer mFeedbackLogBuffer;
private boolean mIsPasswordView = false;
private boolean mIsLoggingSuspended = false;
@@ -135,10 +132,13 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
private Dictionary mDictionary;
private KeyboardSwitcher mKeyboardSwitcher;
private InputMethodService mInputMethodService;
-
+ private final Statistics mStatistics;
private ResearchLogUploader mResearchLogUploader;
+ private LogUnit mCurrentLogUnit = new LogUnit();
+
private ResearchLogger() {
+ mStatistics = Statistics.getInstance();
}
public static ResearchLogger getInstance() {
@@ -259,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('-');
@@ -268,10 +278,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;
@@ -280,73 +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() {
- if (mMainResearchLog != null) {
- mMainResearchLog.stop();
- }
- if (mIntentionalResearchLog != null) {
- mIntentionalResearchLog.stop();
- }
- }
+ logStatistics();
+ commitCurrentLogUnit();
- private void setLoggingAllowed(boolean enableLogging) {
- if (mPrefs == null) {
- return;
+ if (mMainLogBuffer != null) {
+ publishLogBuffer(mMainLogBuffer, mMainResearchLog, false /* isIncludingPrivateData */);
+ mMainResearchLog.close();
+ mMainLogBuffer = null;
+ }
+ 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.
+ // Don't know whether this succeeded or not. We assume not; this is reported
+ // to the caller.
}
- if (mMainResearchLog.isAbortSuccessful()) {
- didAbortMainLog = true;
- }
- 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.
- }
- if (mIntentionalResearchLog.isAbortSuccessful()) {
- didAbortIntentionalLog = true;
+ // Don't know whether this succeeded or not. We assume not; this is reported
+ // to the caller.
}
- mIntentionalResearchLog = null;
- }
- return didAbortMainLog && didAbortIntentionalLog;
- }
-
- /* package */ void flush() {
- if (mMainResearchLog != null) {
- mMainResearchLog.flush();
+ mFeedbackLogBuffer = null;
}
+ return didAbortMainLog && didAbortFeedbackLog;
}
private void restart() {
@@ -450,79 +470,39 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
latinIME.launchKeyboardedDialogActivity(FeedbackActivity.class);
}
- private ResearchLog mFeedbackLog;
- private List<LogUnit> mFeedbackQueue;
- private ResearchLog mSavedMainResearchLog;
- private ResearchLog mSavedIntentionalResearchLog;
- private List<LogUnit> mSavedIntentionalResearchLogQueue;
-
- private void saveLogsForFeedback() {
- mFeedbackLog = mIntentionalResearchLog;
- if (mIntentionalResearchLogQueue != null) {
- mFeedbackQueue = new ArrayList<LogUnit>(mIntentionalResearchLogQueue);
- } else {
- mFeedbackQueue = null;
- }
- mSavedMainResearchLog = mMainResearchLog;
- mSavedIntentionalResearchLog = mIntentionalResearchLog;
- mSavedIntentionalResearchLogQueue = mIntentionalResearchLogQueue;
-
- mMainResearchLog = null;
- mIntentionalResearchLog = null;
- mIntentionalResearchLogQueue = new ArrayList<LogUnit>();
- }
-
- 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) {
@@ -530,7 +510,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
}
private boolean isAllowedToLog() {
- return !mIsPasswordView && !mIsLoggingSuspended && sIsLogging;
+ return !mIsPasswordView && !mIsLoggingSuspended && sIsLogging && !mInFeedbackDialog;
}
public void requestIndicatorRedraw() {
@@ -577,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.
*
@@ -599,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.
*
@@ -618,139 +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;
+ final int codePoint = word.codePointAt(i);
+ if (!Character.isLetter(codePoint)) {
+ return false;
}
- 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();
- }
- 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;
+ return true;
}
- private void onWordComplete(String word) {
- if (isPrivacyThreat(word)) {
- publishLogUnit(mCurrentLogUnit, true);
- mLoggingFrequencyState.onWordNotLogged();
- } else {
- publishLogUnit(mCurrentLogUnit, false);
- mLoggingFrequencyState.onWordLogged();
- }
- mCurrentLogUnit = new LogUnit();
- }
-
- 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<String[]> mKeysList = new ArrayList<String[]>();
- private final List<Object[]> mValuesList = new ArrayList<Object[]>();
- private final List<Boolean> mIsPotentiallyPrivate = new ArrayList<Boolean>();
-
- 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) {
@@ -803,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"
@@ -816,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;
@@ -846,34 +731,10 @@ 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"
};
- 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 = {
@@ -908,51 +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 Object[] values = {
- Keyboard.printableCode(scrubDigitFromCodePoint(code)), x, y
- };
- 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 long time = SystemClock.uptimeMillis();
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
+ Keyboard.printableCode(scrubDigitFromCodePoint(code)), x, y
};
- 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);
+ researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_ONCODEINPUT, values);
+ if (Character.isDigit(code)) {
+ researchLogger.setCurrentLogUnitContainsDigitFlag();
+ }
+ researchLogger.mStatistics.recordChar(code, time);
}
private static final String[] EVENTKEYS_LATINIME_ONDISPLAYCOMPLETIONS = {
@@ -979,6 +805,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);
@@ -1013,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();
}
}
@@ -1048,29 +876,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 +901,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"
};
@@ -1118,15 +908,18 @@ 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_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 +1038,128 @@ 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);
+ }
+
+ // 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", "typedWord", "autoCorrection"
+ };
+ */
+ 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().enqueuePotentiallyPrivateEvent(
+ 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().enqueuePotentiallyPrivateEvent(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) {
+ if (text == null) {
+ throw new RuntimeException("setComposingText is null");
+ }
+ final Object[] values = {
+ text, newCursorPosition
+ };
+ getInstance().enqueuePotentiallyPrivateEvent(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().enqueuePotentiallyPrivateEvent(EVENTKEYS_RICHINPUTCONNECTION_SETSELECTION,
+ values);
+ }
+
private static final String[] EVENTKEYS_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT = {
"SuddenJumpingTouchEventHandlerOnTouchEvent", "motionEvent"
};
@@ -1277,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
new file mode 100644
index 000000000..eab465aa2
--- /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 = 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;
+ // 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;
+ }
+}