diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index ff7654a14..455086015 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,9 +926,6 @@ 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) { @@ -1049,6 +1046,9 @@ 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,9 +1090,12 @@ 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_swapSwapperAndSpace(); + ResearchLogger.latinIME_swapSwapperAndSpaceWhileInBatchEdit(); } mKeyboardSwitcher.updateShiftState(); } @@ -1109,6 +1112,9 @@ 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; } @@ -1172,6 +1178,9 @@ 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() { @@ -1208,9 +1217,6 @@ 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; } @@ -1227,6 +1233,9 @@ 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}. @@ -1238,6 +1247,11 @@ 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 @@ -1319,9 +1333,6 @@ 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 @@ -1335,6 +1346,9 @@ 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); @@ -1433,6 +1447,9 @@ 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. @@ -1452,6 +1469,9 @@ 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()) { @@ -1480,6 +1500,9 @@ 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) { @@ -1498,8 +1521,14 @@ 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)) { @@ -1824,6 +1853,10 @@ 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); @@ -1849,13 +1882,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; } @@ -1882,6 +1915,10 @@ 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; } @@ -1890,12 +1927,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen final String replacedWord = mWordComposer.getTypedWord().toString(); LatinImeLogger.logOnManualSuggestion(replacedWord, suggestion.toString(), index, suggestedWords); - 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); } + mExpectingUpdateSelection = true; + commitChosenWord(suggestion, LastComposedWord.COMMIT_TYPE_MANUAL_PICK, + LastComposedWord.NOT_A_SEPARATOR); mConnection.endBatchEdit(); // Don't allow cancellation of manual pick mLastComposedWord.deactivate(); @@ -1930,6 +1967,9 @@ 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 @@ -1996,6 +2036,9 @@ 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(); } @@ -2023,6 +2066,9 @@ 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 41e59e92d..8b4c17322 100644 --- a/java/src/com/android/inputmethod/latin/RichInputConnection.java +++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java @@ -55,9 +55,7 @@ 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"); @@ -68,9 +66,7 @@ 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() { @@ -83,22 +79,12 @@ public class RichInputConnection { public void finishComposingText() { checkBatchEdit(); - if (null != mIC) { - mIC.finishComposingText(); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.richInputConnection_finishComposingText(); - } - } + if (null != mIC) mIC.finishComposingText(); } public void commitText(final CharSequence text, final int i) { checkBatchEdit(); - if (null != mIC) { - mIC.commitText(text, i); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.richInputConnection_commitText(text, i); - } - } + if (null != mIC) mIC.commitText(text, i); } public int getCursorCapsMode(final int inputType) { @@ -121,72 +107,37 @@ public class RichInputConnection { public void deleteSurroundingText(final int i, final int j) { checkBatchEdit(); - if (null != mIC) { - mIC.deleteSurroundingText(i, j); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.richInputConnection_deleteSurroundingText(i, j); - } - } + if (null != mIC) mIC.deleteSurroundingText(i, j); } public void performEditorAction(final int actionId) { mIC = mParent.getCurrentInputConnection(); - if (null != mIC) { - mIC.performEditorAction(actionId); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.richInputConnection_performEditorAction(actionId); - } - } + if (null != mIC) mIC.performEditorAction(actionId); } public void sendKeyEvent(final KeyEvent keyEvent) { checkBatchEdit(); - if (null != mIC) { - mIC.sendKeyEvent(keyEvent); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.richInputConnection_sendKeyEvent(keyEvent); - } - } + if (null != mIC) mIC.sendKeyEvent(keyEvent); } public void setComposingText(final CharSequence text, final int i) { checkBatchEdit(); - if (null != mIC) { - mIC.setComposingText(text, i); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.richInputConnection_setComposingText(text, i); - } - } + if (null != mIC) mIC.setComposingText(text, i); } public void setSelection(final int from, final int to) { checkBatchEdit(); - if (null != mIC) { - mIC.setSelection(from, to); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.richInputConnection_setSelection(from, to); - } - } + if (null != mIC) mIC.setSelection(from, to); } public void commitCorrection(final CorrectionInfo correctionInfo) { checkBatchEdit(); - if (null != mIC) { - mIC.commitCorrection(correctionInfo); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.richInputConnection_commitCorrection(correctionInfo); - } - } + if (null != mIC) mIC.commitCorrection(correctionInfo); } public void commitCompletion(final CompletionInfo completionInfo) { checkBatchEdit(); - if (null != mIC) { - mIC.commitCompletion(completionInfo); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.richInputConnection_commitCompletion(completionInfo); - } - } + if (null != mIC) mIC.commitCompletion(completionInfo); } public CharSequence getNthPreviousWord(final String sentenceSeperators, final int n) { @@ -364,6 +315,9 @@ 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); + } } } @@ -428,7 +382,13 @@ 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; } @@ -449,7 +409,13 @@ 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 11eae8813..c9f3b476a 100644 --- a/java/src/com/android/inputmethod/research/FeedbackActivity.java +++ b/java/src/com/android/inputmethod/research/FeedbackActivity.java @@ -18,7 +18,10 @@ 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; @@ -28,11 +31,6 @@ 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 deleted file mode 100644 index 65f5f83ae..000000000 --- a/java/src/com/android/inputmethod/research/LogBuffer.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * 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 deleted file mode 100644 index 8a80664f5..000000000 --- a/java/src/com/android/inputmethod/research/LogUnit.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * 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 deleted file mode 100644 index 745768d35..000000000 --- a/java/src/com/android/inputmethod/research/MainLogBuffer.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * 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 71a6d6a78..18bf3c07f 100644 --- a/java/src/com/android/inputmethod/research/ResearchLog.java +++ b/java/src/com/android/inputmethod/research/ResearchLog.java @@ -26,6 +26,7 @@ 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; @@ -36,7 +37,6 @@ 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,22 +51,21 @@ import java.util.concurrent.TimeUnit; */ public class ResearchLog { private static final String TAG = ResearchLog.class.getSimpleName(); - 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; - - /* 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 static final JsonWriter NULL_JSON_WRITER = new JsonWriter( new OutputStreamWriter(new NullOutputStream())); + + final ScheduledExecutorService mExecutor; + /* package */ final File mFile; + private JsonWriter mJsonWriter = NULL_JSON_WRITER; + + 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 class NullOutputStream extends OutputStream { /** {@inheritDoc} */ @Override @@ -85,81 +84,128 @@ public class ResearchLog { } } - public ResearchLog(final File outputFile) { + public ResearchLog(File outputFile) { + mExecutor = Executors.newSingleThreadScheduledExecutor(); if (outputFile == null) { throw new IllegalArgumentException(); } - mExecutor = Executors.newSingleThreadScheduledExecutor(); mFile = outputFile; + mLoggingState = LOGGING_STATE_UNSTARTED; } - 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; + 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; } - } 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(); + mLoggingState = LOGGING_STATE_STOPPING; + break; + case LOGGING_STATE_STOPPING: + case LOGGING_STATE_STOPPED: + } + } + + public boolean isAlive() { + switch (mLoggingState) { + case LOGGING_STATE_UNSTARTED: + case LOGGING_STATE_READY: + case LOGGING_STATE_RUNNING: + return true; + } + return false; + } + + public void waitUntilStopped(final int timeoutInMs) throws InterruptedException { removeAnyScheduledFlush(); mExecutor.shutdown(); + mExecutor.awaitTermination(timeoutInMs, TimeUnit.MILLISECONDS); } - private boolean mIsAbortSuccessful; - public synchronized void abort() { - mExecutor.submit(new Callable() { - @Override - public Object call() throws Exception { - try { - if (mHasWrittenData) { - mJsonWriter.endArray(); - mJsonWriter.close(); - mHasWrittenData = false; + 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; } - } finally { - mIsAbortSuccessful = mFile.delete(); - } - return null; - } - }); - removeAnyScheduledFlush(); - mExecutor.shutdown(); + }); + 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; - } - - public void awaitTermination(int delay, TimeUnit timeUnit) throws InterruptedException { - mExecutor.awaitTermination(delay, timeUnit); + private boolean isAbortSuccessful; + public boolean isAbortSuccessful() { + return isAbortSuccessful; } /* package */ synchronized void flush() { - removeAnyScheduledFlush(); - mExecutor.submit(mFlushCallable); + 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: + } } - private final Callable mFlushCallable = new Callable() { + private Callable mFlushCallable = new Callable() { @Override public Object call() throws Exception { - mJsonWriter.flush(); + if (mLoggingState == LOGGING_STATE_RUNNING) { + mJsonWriter.flush(); + } return null; } }; @@ -178,40 +224,56 @@ public class ResearchLog { mFlushFuture = mExecutor.schedule(mFlushCallable, FLUSH_DELAY_IN_MS, TimeUnit.MILLISECONDS); } - 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. + 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: } } 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. - 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]); - } - } + // not thread safe. 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()); @@ -221,8 +283,8 @@ public class ResearchLog { for (int i = 0; i < length; i++) { mJsonWriter.name(keys[i + 1]); Object value = values[i]; - if (value instanceof CharSequence) { - mJsonWriter.value(value.toString()); + if (value instanceof String) { + mJsonWriter.value((String) value); } else if (value instanceof Number) { mJsonWriter.value((Number) value); } else if (value instanceof Boolean) { @@ -269,11 +331,14 @@ 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(); @@ -298,8 +363,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 9904a1de2..3b1213009 100644 --- a/java/src/com/android/inputmethod/research/ResearchLogUploader.java +++ b/java/src/com/android/inputmethod/research/ResearchLogUploader.java @@ -27,6 +27,7 @@ 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; @@ -47,7 +48,6 @@ 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,6 +55,8 @@ 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; @@ -91,15 +93,11 @@ public final class ResearchLogUploader { public void start() { if (mCanUpload) { - 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 */)); + 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"); } } @@ -108,8 +106,7 @@ 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(null /* logToWaitFor */, callback, - true /* forceUpload */)); + mExecutor.submit(new UploadRunnable(callback, true)); } } @@ -133,33 +130,19 @@ public final class ResearchLogUploader { } class UploadRunnable implements Runnable { - private final ResearchLog mLogToWaitFor; private final Callback mCallback; private final boolean mForceUpload; - public UploadRunnable(final ResearchLog logToWaitFor, final Callback callback, - final boolean forceUpload) { - mLogToWaitFor = logToWaitFor; + public UploadRunnable(final Callback callback, final boolean forceUpload) { 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 3cad2d099..cf6f31a0a 100644 --- a/java/src/com/android/inputmethod/research/ResearchLogger.java +++ b/java/src/com/android/inputmethod/research/ResearchLogger.java @@ -34,18 +34,15 @@ 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; @@ -67,8 +64,11 @@ 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,21 +94,24 @@ 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; - /* package */ ResearchLog mFeedbackLog; - /* package */ MainLogBuffer mMainLogBuffer; - /* package */ LogBuffer mFeedbackLogBuffer; + // 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(); private boolean mIsPasswordView = false; private boolean mIsLoggingSuspended = false; @@ -132,13 +135,10 @@ 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,50 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang e.apply(); } + private File createLogFile(File filesDir) { + final StringBuilder sb = new StringBuilder(); + sb.append(FILENAME_PREFIX).append('-'); + sb.append(mUUIDString).append('-'); + sb.append(TIMESTAMP_DATEFORMAT.format(new Date())); + sb.append(FILENAME_SUFFIX); + return new File(filesDir, sb.toString()); + } + + private void start() { + maybeShowSplashScreen(); + updateSuspendedState(); + requestIndicatorRedraw(); + if (!isAllowedToLog()) { + // Log.w(TAG, "not in usability mode; not logging"); + return; + } + if (mFilesDir == null || !mFilesDir.exists()) { + 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."); + } + } + + /* package */ void stop() { + if (mMainResearchLog != null) { + mMainResearchLog.stop(); + } + if (mIntentionalResearchLog != null) { + mIntentionalResearchLog.stop(); + } + } + private void setLoggingAllowed(boolean enableLogging) { if (mPrefs == null) { return; @@ -269,104 +313,40 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang sIsLogging = enableLogging; } - private File createLogFile(File filesDir) { - final StringBuilder sb = new StringBuilder(); - sb.append(FILENAME_PREFIX).append('-'); - sb.append(mUUIDString).append('-'); - sb.append(TIMESTAMP_DATEFORMAT.format(new Date())); - sb.append(FILENAME_SUFFIX); - 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; - } - if (mFilesDir == null || !mFilesDir.exists()) { - Log.w(TAG, "IME storage directory does not exist. Cannot start logging."); - return; - } - 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(); - commitCurrentLogUnit(); - - if (mMainLogBuffer != null) { - publishLogBuffer(mMainLogBuffer, mMainResearchLog, false /* isIncludingPrivateData */); - mMainResearchLog.close(); - mMainLogBuffer = null; - } - if (mFeedbackLogBuffer != null) { - mFeedbackLog.close(); - mFeedbackLogBuffer = null; - } - } - public boolean abort() { boolean didAbortMainLog = false; - if (mMainLogBuffer != null) { - mMainLogBuffer.clear(); + if (mMainResearchLog != null) { + mMainResearchLog.abort(); try { - didAbortMainLog = mMainResearchLog.blockingAbort(); + mMainResearchLog.waitUntilStopped(ABORT_TIMEOUT_IN_MS); } catch (InterruptedException e) { - // Don't know whether this succeeded or not. We assume not; this is reported - // to the caller. + // interrupted early. carry on. } - mMainLogBuffer = null; + if (mMainResearchLog.isAbortSuccessful()) { + didAbortMainLog = true; + } + mMainResearchLog = null; } - boolean didAbortFeedbackLog = false; - if (mFeedbackLogBuffer != null) { - mFeedbackLogBuffer.clear(); + boolean didAbortIntentionalLog = false; + if (mIntentionalResearchLog != null) { + mIntentionalResearchLog.abort(); try { - didAbortFeedbackLog = mFeedbackLog.blockingAbort(); + mIntentionalResearchLog.waitUntilStopped(ABORT_TIMEOUT_IN_MS); } catch (InterruptedException e) { - // Don't know whether this succeeded or not. We assume not; this is reported - // to the caller. + // interrupted early. carry on. } - mFeedbackLogBuffer = null; + if (mIntentionalResearchLog.isAbortSuccessful()) { + didAbortIntentionalLog = true; + } + mIntentionalResearchLog = null; + } + return didAbortMainLog && didAbortIntentionalLog; + } + + /* package */ void flush() { + if (mMainResearchLog != null) { + mMainResearchLog.flush(); } - return didAbortMainLog && didAbortFeedbackLog; } private void restart() { @@ -470,39 +450,79 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang latinIME.launchKeyboardedDialogActivity(FeedbackActivity.class); } - private static final String[] EVENTKEYS_FEEDBACK = { - "UserTimestamp", "contents" - }; + 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; public void sendFeedback(final String feedbackContents, final boolean includeHistory) { - if (mFeedbackLogBuffer == null) { - return; + 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 (!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) { @@ -510,7 +530,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } private boolean isAllowedToLog() { - return !mIsPasswordView && !mIsLoggingSuspended && sIsLogging && !mInFeedbackDialog; + return !mIsPasswordView && !mIsLoggingSuspended && sIsLogging; } public void requestIndicatorRedraw() { @@ -557,8 +577,13 @@ 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. * @@ -574,14 +599,10 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang final Object[] values) { assert values.length + 1 == keys.length; if (isAllowedToLog()) { - mCurrentLogUnit.addLogStatement(keys, values, true /* isPotentiallyPrivate */); + mCurrentLogUnit.addLogAtom(keys, values, true); } } - private void setCurrentLogUnitContainsDigitFlag() { - mCurrentLogUnit.setContainsDigit(); - } - /** * Buffer a research log event, flaggint it as not privacy-sensitive. * @@ -597,54 +618,139 @@ 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.addLogStatement(keys, values, false /* isPotentiallyPrivate */); + mCurrentLogUnit.addLogAtom(keys, values, false); } } - /* 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(); + // 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--; + } + } + + public boolean isSafeToLog() { + return mWordsRemainingToSkip <= 1; + } + } + + /* 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; + } + 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(); + } + 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(); + } + + 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)); } } - if (mFeedbackLogBuffer != null) { - mFeedbackLogBuffer.shiftIn(mCurrentLogUnit); - } - mCurrentLogUnit = new LogUnit(); - Log.d(TAG, "commitCurrentLogUnit"); } - } - /* 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); - } - } - - private boolean hasOnlyLetters(final String word) { - final int length = word.length(); - for (int i = 0; i < length; i = word.offsetByCodePoints(i, 1)) { - final int codePoint = word.codePointAt(i); - if (!Character.isLetter(codePoint)) { - return false; + 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)); } } - return true; - } - - 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) { @@ -697,6 +803,12 @@ 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" @@ -704,6 +816,9 @@ 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; @@ -731,10 +846,34 @@ 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 = { @@ -769,16 +908,51 @@ 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 }; - researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_ONCODEINPUT, values); - if (Character.isDigit(code)) { - researchLogger.setCurrentLogUnitContainsDigitFlag(); - } - researchLogger.mStatistics.recordChar(code, time); + 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 = { @@ -805,10 +979,6 @@ 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); @@ -843,7 +1013,9 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } final ResearchLogger researchLogger = getInstance(); researchLogger.enqueueEvent(EVENTKEYS_LATINIME_ONWINDOWHIDDEN, values); - researchLogger.commitCurrentLogUnit(); + // Play it safe. Remove privacy-sensitive events. + researchLogger.publishLogUnit(researchLogger.mCurrentLogUnit, true); + researchLogger.mCurrentLogUnit = new LogUnit(); getInstance().stop(); } } @@ -876,6 +1048,29 @@ 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" }; @@ -901,6 +1096,21 @@ 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" }; @@ -908,18 +1118,15 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang final Object[] values = { Keyboard.printableCode(scrubDigitFromCodePoint(code)) }; - final ResearchLogger researchLogger = getInstance(); - researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_SENDKEYCODEPOINT, values); - if (Character.isDigit(code)) { - researchLogger.setCurrentLogUnitContainsDigitFlag(); - } + getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_SENDKEYCODEPOINT, values); } - private static final String[] EVENTKEYS_LATINIME_SWAPSWAPPERANDSPACE = { - "LatinIMESwapSwapperAndSpace" + private static final String[] EVENTKEYS_LATINIME_SWAPSWAPPERANDSPACEWHILEINBATCHEDIT = { + "LatinIMESwapSwapperAndSpaceWhileInBatchEdit" }; - public static void latinIME_swapSwapperAndSpace() { - getInstance().enqueueEvent(EVENTKEYS_LATINIME_SWAPSWAPPERANDSPACE, EVENTKEYS_NULLVALUES); + public static void latinIME_swapSwapperAndSpaceWhileInBatchEdit() { + getInstance().enqueueEvent(EVENTKEYS_LATINIME_SWAPSWAPPERANDSPACEWHILEINBATCHEDIT, + EVENTKEYS_NULLVALUES); } private static final String[] EVENTKEYS_MAINKEYBOARDVIEW_ONLONGPRESS = { @@ -1038,128 +1245,6 @@ 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" }; @@ -1192,24 +1277,4 @@ 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 deleted file mode 100644 index eab465aa2..000000000 --- a/java/src/com/android/inputmethod/research/Statistics.java +++ /dev/null @@ -1,146 +0,0 @@ -/* - * 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; - } -}