diff options
Diffstat (limited to 'java/src/com/android/inputmethod/research')
7 files changed, 472 insertions, 152 deletions
diff --git a/java/src/com/android/inputmethod/research/FixedLogBuffer.java b/java/src/com/android/inputmethod/research/FixedLogBuffer.java index f3302d856..777111947 100644 --- a/java/src/com/android/inputmethod/research/FixedLogBuffer.java +++ b/java/src/com/android/inputmethod/research/FixedLogBuffer.java @@ -72,7 +72,16 @@ public class FixedLogBuffer extends LogBuffer { mNumActualWords++; // Must be a word, or we wouldn't be here. } - private void shiftOutThroughFirstWord() { + @Override + public LogUnit unshiftIn() { + final LogUnit logUnit = super.unshiftIn(); + if (logUnit != null && logUnit.hasWord()) { + mNumActualWords--; + } + return logUnit; + } + + public void shiftOutThroughFirstWord() { final LinkedList<LogUnit> logUnits = getLogUnits(); while (!logUnits.isEmpty()) { final LogUnit logUnit = logUnits.removeFirst(); diff --git a/java/src/com/android/inputmethod/research/LogBuffer.java b/java/src/com/android/inputmethod/research/LogBuffer.java index 14e8d08a2..9d095f8ad 100644 --- a/java/src/com/android/inputmethod/research/LogBuffer.java +++ b/java/src/com/android/inputmethod/research/LogBuffer.java @@ -46,6 +46,20 @@ public class LogBuffer { mLogUnits.add(logUnit); } + public LogUnit unshiftIn() { + if (mLogUnits.isEmpty()) { + return null; + } + return mLogUnits.removeLast(); + } + + public LogUnit peekLastLogUnit() { + if (mLogUnits.isEmpty()) { + return null; + } + return mLogUnits.peekLast(); + } + public boolean isEmpty() { return mLogUnits.isEmpty(); } diff --git a/java/src/com/android/inputmethod/research/LogUnit.java b/java/src/com/android/inputmethod/research/LogUnit.java index bcb144f5f..cfba28909 100644 --- a/java/src/com/android/inputmethod/research/LogUnit.java +++ b/java/src/com/android/inputmethod/research/LogUnit.java @@ -114,24 +114,37 @@ import java.util.Map; debugStringWriter = null; debugJsonWriter = null; } - final int size = mLogStatementList.size(); // Write out any logStatement that passes the privacy filter. - for (int i = 0; i < size; i++) { - final LogStatement logStatement = mLogStatementList.get(i); - if (!isIncludingPrivateData && logStatement.mIsPotentiallyPrivate) { - continue; - } - if (mIsPartOfMegaword && logStatement.mIsPotentiallyRevealing) { - continue; + final int size = mLogStatementList.size(); + if (size != 0) { + // Note that jsonWriter is only set to a non-null value if the logUnit start text is + // output and at least one logStatement is output. + JsonWriter jsonWriter = null; + for (int i = 0; i < size; i++) { + final LogStatement logStatement = mLogStatementList.get(i); + if (!isIncludingPrivateData && logStatement.mIsPotentiallyPrivate) { + continue; + } + if (mIsPartOfMegaword && logStatement.mIsPotentiallyRevealing) { + continue; + } + // Only retrieve the jsonWriter if we need to. If we don't get this far, then + // researchLog.getValidJsonWriterLocked() will not ever be called, and the file + // will not have been opened for writing. + if (jsonWriter == null) { + jsonWriter = researchLog.getValidJsonWriterLocked(); + outputLogUnitStart(jsonWriter, isIncludingPrivateData); + } + outputLogStatementToLocked(jsonWriter, mLogStatementList.get(i), mValuesList.get(i), + mTimeList.get(i)); + if (DEBUG) { + outputLogStatementToLocked(debugJsonWriter, mLogStatementList.get(i), + mValuesList.get(i), mTimeList.get(i)); + } } - // Only retrieve the jsonWriter if we need to. If we don't get this far, then - // researchLog.getValidJsonWriter() will not open the file for writing. - final JsonWriter jsonWriter = researchLog.getValidJsonWriterLocked(); - outputLogStatementToLocked(jsonWriter, mLogStatementList.get(i), mValuesList.get(i), - mTimeList.get(i)); - if (DEBUG) { - outputLogStatementToLocked(debugJsonWriter, mLogStatementList.get(i), - mValuesList.get(i), mTimeList.get(i)); + if (jsonWriter != null) { + // We must have called logUnitStart earlier, so emit a logUnitStop. + outputLogUnitStop(jsonWriter, isIncludingPrivateData); } } if (DEBUG) { @@ -152,6 +165,38 @@ import java.util.Map; 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 String WORD_KEY = "_wo"; + private static final String LOG_UNIT_BEGIN_KEY = "logUnitStart"; + private static final String LOG_UNIT_END_KEY = "logUnitEnd"; + + private void outputLogUnitStart(final JsonWriter jsonWriter, + final boolean isIncludingPrivateData) { + try { + jsonWriter.beginObject(); + jsonWriter.name(CURRENT_TIME_KEY).value(System.currentTimeMillis()); + if (isIncludingPrivateData) { + jsonWriter.name(WORD_KEY).value(getWord()); + } + jsonWriter.name(EVENT_TYPE_KEY).value(LOG_UNIT_BEGIN_KEY); + jsonWriter.endObject(); + } catch (IOException e) { + e.printStackTrace(); + Log.w(TAG, "Error in JsonWriter; cannot write LogUnitStart"); + } + } + + private void outputLogUnitStop(final JsonWriter jsonWriter, + final boolean isIncludingPrivateData) { + try { + jsonWriter.beginObject(); + jsonWriter.name(CURRENT_TIME_KEY).value(System.currentTimeMillis()); + jsonWriter.name(EVENT_TYPE_KEY).value(LOG_UNIT_END_KEY); + jsonWriter.endObject(); + } catch (IOException e) { + e.printStackTrace(); + Log.w(TAG, "Error in JsonWriter; cannot write LogUnitStop"); + } + } /** * Write the logStatement and its contents out through jsonWriter. @@ -240,6 +285,7 @@ import java.util.Map; public LogUnit splitByTime(final long maxTime) { // Assume that mTimeList is in sorted order. final int length = mTimeList.size(); + // TODO: find time by binary search, e.g. using Collections#binarySearch() for (int index = 0; index < length; index++) { if (mTimeList.get(index) > maxTime) { final List<LogStatement> laterLogStatements = @@ -267,4 +313,13 @@ import java.util.Map; } return new LogUnit(); } + + public void append(final LogUnit logUnit) { + mLogStatementList.addAll(logUnit.mLogStatementList); + mValuesList.addAll(logUnit.mValuesList); + mTimeList.addAll(logUnit.mTimeList); + mWord = null; + mMayContainDigit = mMayContainDigit || logUnit.mMayContainDigit; + mIsPartOfMegaword = false; + } } diff --git a/java/src/com/android/inputmethod/research/MainLogBuffer.java b/java/src/com/android/inputmethod/research/MainLogBuffer.java index bec21d7e0..a8f255a41 100644 --- a/java/src/com/android/inputmethod/research/MainLogBuffer.java +++ b/java/src/com/android/inputmethod/research/MainLogBuffer.java @@ -26,18 +26,42 @@ import java.util.LinkedList; import java.util.Random; /** - * Provide a log buffer of fixed length that enforces privacy restrictions. + * MainLogBuffer is a FixedLogBuffer that tracks the state of LogUnits to make privacy guarantees. * - * The privacy restrictions include making sure that no numbers are logged, that all logged words - * are in the dictionary, and that words are recorded infrequently enough that the user's meaning - * cannot be easily determined. + * There are three forms of privacy protection: 1) only words in the main dictionary are allowed to + * be logged in enough detail to determine their contents, 2) only a subset of words are logged + * in detail, such as 10%, and 3) no numbers are logged. + * + * This class maintains a list of LogUnits, each corresponding to a word. As the user completes + * words, they are added here. But if the user backs up over their current word to edit a word + * entered earlier, then it is pulled out of this LogBuffer, changes are then added to the end of + * the LogUnit, and it is pushed back in here when the user is done. Because words may be pulled + * back out even after they are pushed in, we must not publish the contents of this LogBuffer too + * quickly. However, we cannot let the contents pile up either, or it will limit the editing that + * a user can perform. + * + * To balance these requirements (keep history so user can edit, flush history so it does not pile + * up), the LogBuffer is considered "complete" when the user has entered enough words to form an + * n-gram, followed by enough additional non-detailed words (that are in the 90%, as per above). + * Once complete, the n-gram may be published to flash storage (via the ResearchLog class). + * However, the additional non-detailed words are retained, in case the user backspaces to edit + * them. The MainLogBuffer then continues to add words, publishing individual non-detailed words + * as new words arrive. After enough non-detailed words have been pushed out to account for the + * 90% between words, the words at the front of the LogBuffer can be published as an n-gram again. + * + * If the words that would form the valid n-gram are not in the dictionary, then words are pushed + * through the LogBuffer one at a time until an n-gram is found that is entirely composed of + * dictionary words. + * + * If the user closes a session, then the entire LogBuffer is flushed, publishing any embedded + * n-gram containing dictionary words. */ public class MainLogBuffer extends FixedLogBuffer { private static final String TAG = MainLogBuffer.class.getSimpleName(); private static final boolean DEBUG = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG; // 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; + public static final int N_GRAM_SIZE = 2; // The number of words between n-grams to omit from the log. If debugging, record 50% of all // words. Otherwise, only record 10%. private static final int DEFAULT_NUMBER_OF_WORDS_BETWEEN_SAMPLES = @@ -46,49 +70,31 @@ public class MainLogBuffer extends FixedLogBuffer { 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; + /* package for test */ int mNumWordsBetweenNGrams; // 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; + /* package for test */ int mNumWordsUntilSafeToSample; public MainLogBuffer(final ResearchLog researchLog) { - super(N_GRAM_SIZE); + super(N_GRAM_SIZE + DEFAULT_NUMBER_OF_WORDS_BETWEEN_SAMPLES); mResearchLog = researchLog; - mMinWordPeriod = DEFAULT_NUMBER_OF_WORDS_BETWEEN_SAMPLES + N_GRAM_SIZE; + mNumWordsBetweenNGrams = DEFAULT_NUMBER_OF_WORDS_BETWEEN_SAMPLES; final Random random = new Random(); - mWordsUntilSafeToSample = random.nextInt(mMinWordPeriod); + mNumWordsUntilSafeToSample = DEBUG ? 0 : random.nextInt(mNumWordsBetweenNGrams + 1); } public void setSuggest(final Suggest suggest) { mSuggest = suggest; } - @Override - public void shiftIn(final LogUnit newLogUnit) { - super.shiftIn(newLogUnit); - if (newLogUnit.hasWord()) { - if (mWordsUntilSafeToSample > 0) { - mWordsUntilSafeToSample--; - } - } - if (DEBUG) { - Log.d(TAG, "shiftedIn " + (newLogUnit.hasWord() ? newLogUnit.getWord() : "")); - } - } - public void resetWordCounter() { - mWordsUntilSafeToSample = mMinWordPeriod; + mNumWordsUntilSafeToSample = mNumWordsBetweenNGrams; } /** - * Determines whether the content of the MainLogBuffer can be safely uploaded in its complete - * form and still protect the user's privacy. + * Determines whether uploading the n words at the front the MainLogBuffer will not violate + * user 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 @@ -97,10 +103,10 @@ public class MainLogBuffer extends FixedLogBuffer { * the screen orientation and other characteristics about the device can be uploaded without * revealing much about the user. */ - public boolean isSafeToLog() { + public boolean isNGramSafe() { // Check that we are not sampling too frequently. Having sampled recently might disclose // too much of the user's intended meaning. - if (mWordsUntilSafeToSample > 0) { + if (mNumWordsUntilSafeToSample > 0) { return false; } if (mSuggest == null || !mSuggest.hasMainDictionary()) { @@ -119,7 +125,8 @@ public class MainLogBuffer extends FixedLogBuffer { // complete buffer contents in detail. final LinkedList<LogUnit> logUnits = getLogUnits(); final int length = logUnits.size(); - for (int i = 0; i < length; i++) { + int wordsNeeded = N_GRAM_SIZE; + for (int i = 0; i < length && wordsNeeded > 0; i++) { final LogUnit logUnit = logUnits.get(i); final String word = logUnit.getWord(); if (word == null) { @@ -142,10 +149,34 @@ public class MainLogBuffer extends FixedLogBuffer { return true; } + public boolean isNGramComplete() { + final LinkedList<LogUnit> logUnits = getLogUnits(); + final int length = logUnits.size(); + int wordsNeeded = N_GRAM_SIZE; + for (int i = 0; i < length && wordsNeeded > 0; i++) { + final LogUnit logUnit = logUnits.get(i); + final String word = logUnit.getWord(); + if (word != null) { + wordsNeeded--; + } + } + return wordsNeeded == 0; + } + @Override protected void onShiftOut(final LogUnit logUnit) { if (mResearchLog != null) { - mResearchLog.publish(logUnit, false /* isIncludingPrivateData */); + mResearchLog.publish(logUnit, + ResearchLogger.IS_LOGGING_EVERYTHING /* isIncludingPrivateData */); + } + if (logUnit.hasWord()) { + if (mNumWordsUntilSafeToSample > 0) { + mNumWordsUntilSafeToSample--; + Log.d(TAG, "wordsUntilSafeToSample now at " + mNumWordsUntilSafeToSample); + } + } + if (DEBUG) { + Log.d(TAG, "shiftedOut " + (logUnit.hasWord() ? logUnit.getWord() : "")); } } } diff --git a/java/src/com/android/inputmethod/research/ResearchLog.java b/java/src/com/android/inputmethod/research/ResearchLog.java index a6b1b889f..5edb46e27 100644 --- a/java/src/com/android/inputmethod/research/ResearchLog.java +++ b/java/src/com/android/inputmethod/research/ResearchLog.java @@ -16,6 +16,7 @@ package com.android.inputmethod.research; +import android.content.Context; import android.util.JsonWriter; import android.util.Log; @@ -23,7 +24,7 @@ import com.android.inputmethod.latin.define.ProductionFlag; import java.io.BufferedWriter; import java.io.File; -import java.io.FileWriter; +import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; @@ -50,6 +51,8 @@ public class ResearchLog { /* package */ final ScheduledExecutorService mExecutor; /* package */ final File mFile; + private final Context mContext; + 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 @@ -78,12 +81,13 @@ public class ResearchLog { } } - public ResearchLog(final File outputFile) { + public ResearchLog(final File outputFile, Context context) { if (outputFile == null) { throw new IllegalArgumentException(); } mExecutor = Executors.newSingleThreadScheduledExecutor(); mFile = outputFile; + mContext = context; } public synchronized void close(final Runnable onClosed) { @@ -193,6 +197,9 @@ public class ResearchLog { }); } catch (RejectedExecutionException e) { // TODO: Add code to record loss of data, and report. + if (DEBUG) { + Log.d(TAG, "ResearchLog.publish() rejecting scheduled execution"); + } } } @@ -203,7 +210,9 @@ public class ResearchLog { public JsonWriter getValidJsonWriterLocked() { try { if (mJsonWriter == NULL_JSON_WRITER) { - mJsonWriter = new JsonWriter(new BufferedWriter(new FileWriter(mFile))); + final FileOutputStream fos = + mContext.openFileOutput(mFile.getName(), Context.MODE_PRIVATE); + mJsonWriter = new JsonWriter(new BufferedWriter(new OutputStreamWriter(fos))); mJsonWriter.beginArray(); mHasWrittenData = true; } diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java index b1484e696..f4249a045 100644 --- a/java/src/com/android/inputmethod/research/ResearchLogger.java +++ b/java/src/com/android/inputmethod/research/ResearchLogger.java @@ -85,7 +85,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang private static final String TAG = ResearchLogger.class.getSimpleName(); private static final boolean DEBUG = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG; // Whether all n-grams should be logged. true will disclose private info. - private static final boolean LOG_EVERYTHING = false + public static final boolean IS_LOGGING_EVERYTHING = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG; // Whether the TextView contents are logged at the end of the session. true will disclose // private info. @@ -105,7 +105,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang private static final boolean IS_SHOWING_INDICATOR = true; // Change the default indicator to something very visible. Currently two red vertical bars on // either side of they keyboard. - private static final boolean IS_SHOWING_INDICATOR_CLEARLY = false || LOG_EVERYTHING; + private static final boolean IS_SHOWING_INDICATOR_CLEARLY = false || IS_LOGGING_EVERYTHING; public static final int FEEDBACK_WORD_BUFFER_SIZE = 5; // constants related to specific log points @@ -324,11 +324,22 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang sIsLogging = enableLogging; } + private static int sLogFileCounter = 0; + 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(TIMESTAMP_DATEFORMAT.format(new Date())).append('-'); + // Sometimes logFiles are created within milliseconds of each other. Append a counter to + // separate these. + if (sLogFileCounter < Integer.MAX_VALUE) { + sLogFileCounter++; + } else { + // Wrap the counter, in the unlikely event of overflow. + sLogFileCounter = 0; + } + sb.append(sLogFileCounter); sb.append(FILENAME_SUFFIX); return new File(filesDir, sb.toString()); } @@ -374,12 +385,12 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang return; } if (mMainLogBuffer == null) { - mMainResearchLog = new ResearchLog(createLogFile(mFilesDir)); + mMainResearchLog = new ResearchLog(createLogFile(mFilesDir), mLatinIME); mMainLogBuffer = new MainLogBuffer(mMainResearchLog); mMainLogBuffer.setSuggest(mSuggest); } if (mFeedbackLogBuffer == null) { - mFeedbackLog = new ResearchLog(createLogFile(mFilesDir)); + mFeedbackLog = new ResearchLog(createLogFile(mFilesDir), mLatinIME); // LogBuffer is one more than FEEDBACK_WORD_BUFFER_SIZE, because it must also hold // the feedback LogUnit itself. mFeedbackLogBuffer = new FixedLogBuffer(FEEDBACK_WORD_BUFFER_SIZE + 1); @@ -390,11 +401,20 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang if (DEBUG) { Log.d(TAG, "stop called"); } + // Commit mCurrentLogUnit before closing. commitCurrentLogUnit(); if (mMainLogBuffer != null) { - publishLogBuffer(mMainLogBuffer, mMainResearchLog, - LOG_EVERYTHING /* isIncludingPrivateData */); + while (!mMainLogBuffer.isEmpty()) { + if ((mMainLogBuffer.isNGramSafe() || IS_LOGGING_EVERYTHING) && + mMainResearchLog != null) { + publishLogBuffer(mMainLogBuffer, mMainResearchLog, + true /* isIncludingPrivateData */); + mMainLogBuffer.resetWordCounter(); + } else { + mMainLogBuffer.shiftOutThroughFirstWord(); + } + } mMainResearchLog.close(null /* callback */); mMainLogBuffer = null; } @@ -590,7 +610,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang uploadNow(); } }); - mFeedbackLog = new ResearchLog(createLogFile(mFilesDir)); + mFeedbackLog = new ResearchLog(createLogFile(mFilesDir), mLatinIME); } public void uploadNow() { @@ -676,11 +696,17 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang /** * Buffer a research log event, flagging it as privacy-sensitive. */ - private synchronized void enqueueEvent(LogStatement logStatement, Object... values) { + private synchronized void enqueueEvent(final LogStatement logStatement, + final Object... values) { + enqueueEvent(mCurrentLogUnit, logStatement, values); + } + + private synchronized void enqueueEvent(final LogUnit logUnit, final LogStatement logStatement, + final Object... values) { assert values.length == logStatement.mKeys.length; - if (isAllowedToLog()) { + if (isAllowedToLog() && logUnit != null) { final long time = SystemClock.uptimeMillis(); - mCurrentLogUnit.addLogStatement(logStatement, time, values); + logUnit.addLogStatement(logStatement, time, values); } } @@ -695,17 +721,70 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } if (!mCurrentLogUnit.isEmpty()) { if (mMainLogBuffer != null) { - mMainLogBuffer.shiftIn(mCurrentLogUnit); - if ((mMainLogBuffer.isSafeToLog() || LOG_EVERYTHING) && mMainResearchLog != null) { + if ((mMainLogBuffer.isNGramSafe() || IS_LOGGING_EVERYTHING) && + mMainLogBuffer.isNGramComplete() && + mMainResearchLog != null) { publishLogBuffer(mMainLogBuffer, mMainResearchLog, true /* isIncludingPrivateData */); mMainLogBuffer.resetWordCounter(); } + mMainLogBuffer.shiftIn(mCurrentLogUnit); } if (mFeedbackLogBuffer != null) { mFeedbackLogBuffer.shiftIn(mCurrentLogUnit); } mCurrentLogUnit = new LogUnit(); + } else { + if (DEBUG) { + Log.d(TAG, "Warning: tried to commit empty log unit."); + } + } + } + + private static final LogStatement LOGSTATEMENT_UNCOMMIT_CURRENT_LOGUNIT = + new LogStatement("UncommitCurrentLogUnit", false, false); + public void uncommitCurrentLogUnit(final String expectedWord, + final boolean dumpCurrentLogUnit) { + // The user has deleted this word and returned to the previous. Check that the word in the + // logUnit matches the expected word. If so, restore the last log unit committed to be the + // current logUnit. I.e., pull out the last LogUnit from all the LogBuffers, and make + // restore it to mCurrentLogUnit so the new edits are captured with the word. Optionally + // dump the contents of mCurrentLogUnit (useful if they contain deletions of the next word + // that should not be reported to protect user privacy) + // + // Note that we don't use mLastLogUnit here, because it only goes one word back and is only + // needed for reverts, which only happen one back. + if (mMainLogBuffer == null) { + return; + } + final LogUnit oldLogUnit = mMainLogBuffer.peekLastLogUnit(); + + // Check that expected word matches. + if (oldLogUnit != null) { + final String oldLogUnitWord = oldLogUnit.getWord(); + if (!oldLogUnitWord.equals(expectedWord)) { + return; + } + } + + // Uncommit, merging if necessary. + mMainLogBuffer.unshiftIn(); + if (oldLogUnit != null && !dumpCurrentLogUnit) { + oldLogUnit.append(mCurrentLogUnit); + mSavedDownEventTime = Long.MAX_VALUE; + } + if (oldLogUnit == null) { + mCurrentLogUnit = new LogUnit(); + } else { + mCurrentLogUnit = oldLogUnit; + } + if (mFeedbackLogBuffer != null) { + mFeedbackLogBuffer.unshiftIn(); + } + enqueueEvent(LOGSTATEMENT_UNCOMMIT_CURRENT_LOGUNIT); + if (DEBUG) { + Log.d(TAG, "uncommitCurrentLogUnit (dump=" + dumpCurrentLogUnit + ") back to " + + (mCurrentLogUnit.hasWord() ? ": '" + mCurrentLogUnit.getWord() + "'" : "")); } } @@ -721,12 +800,16 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang isIncludingPrivateData); researchLog.publish(openingLogUnit, true /* isIncludingPrivateData */); LogUnit logUnit; - while ((logUnit = logBuffer.shiftOut()) != null) { + int numWordsToPublish = MainLogBuffer.N_GRAM_SIZE; + while ((logUnit = logBuffer.shiftOut()) != null && numWordsToPublish > 0) { if (DEBUG) { Log.d(TAG, "publishLogBuffer: " + (logUnit.hasWord() ? logUnit.getWord() : "<wordless>")); } researchLog.publish(logUnit, isIncludingPrivateData); + if (logUnit.getWord() != null) { + numWordsToPublish--; + } } final LogUnit closingLogUnit = new LogUnit(); closingLogUnit.addLogStatement(LOGSTATEMENT_LOG_SEGMENT_CLOSING, @@ -751,24 +834,30 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang * After this operation completes, mCurrentLogUnit will hold any logStatements that happened * after maxTime. */ - private static final LogStatement LOGSTATEMENT_COMMIT_RECORD_SPLIT_WORDS = - new LogStatement("recordSplitWords", true, false); - /* package for test */ void commitCurrentLogUnitAsWord(final String word, final long maxTime) { + /* package for test */ void commitCurrentLogUnitAsWord(final String word, final long maxTime, + final boolean isBatchMode) { + if (word == null) { + return; + } final Dictionary dictionary = getDictionary(); - if (word != null && word.length() > 0 && hasLetters(word)) { + if (word.length() > 0 && hasLetters(word)) { mCurrentLogUnit.setWord(word); final boolean isDictionaryWord = dictionary != null && dictionary.isValidWord(word); mStatistics.recordWordEntered(isDictionaryWord); } final LogUnit newLogUnit = mCurrentLogUnit.splitByTime(maxTime); - enqueueCommitText(word); + enqueueCommitText(word, isBatchMode); commitCurrentLogUnit(); mCurrentLogUnit = newLogUnit; } - public void onWordFinished(final String word) { - commitCurrentLogUnitAsWord(word, mSavedDownEventTime); + private void setSavedDownEventTime(final long time) { + mSavedDownEventTime = time; + } + + public void onWordFinished(final String word, final boolean isBatchMode) { + commitCurrentLogUnitAsWord(word, mSavedDownEventTime, isBatchMode); mSavedDownEventTime = Long.MAX_VALUE; } @@ -863,7 +952,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang Integer.toHexString(editorInfo.inputType), Integer.toHexString(editorInfo.imeOptions), editorInfo.fieldId, Build.DISPLAY, Build.MODEL, prefs, versionCode, versionName, - OUTPUT_FORMAT_VERSION, LOG_EVERYTHING, + OUTPUT_FORMAT_VERSION, IS_LOGGING_EVERYTHING, ProductionFlag.IS_EXPERIMENTAL_DEBUG); } catch (NameNotFoundException e) { e.printStackTrace(); @@ -916,7 +1005,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang if (action == MotionEvent.ACTION_DOWN) { // Subtract 1 from eventTime so the down event is included in the later // LogUnit, not the earlier (the test is for inequality). - researchLogger.mSavedDownEventTime = eventTime - 1; + researchLogger.setSavedDownEventTime(eventTime - 1); } } } @@ -1060,9 +1149,9 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang * * SystemResponse: Raw text is added to the TextView. */ - public static void latinIME_onTextInput(final String text) { + public static void latinIME_onTextInput(final String text, final boolean isBatchMode) { final ResearchLogger researchLogger = getInstance(); - researchLogger.commitCurrentLogUnitAsWord(text, Long.MAX_VALUE); + researchLogger.commitCurrentLogUnitAsWord(text, Long.MAX_VALUE, isBatchMode); } /** @@ -1074,15 +1163,15 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang new LogStatement("LatinIMEPickSuggestionManually", true, false, "replacedWord", "index", "suggestion", "x", "y"); public static void latinIME_pickSuggestionManually(final String replacedWord, - final int index, final String suggestion) { + final int index, final String suggestion, final boolean isBatchMode) { final String scrubbedWord = scrubDigitsFromString(suggestion); final ResearchLogger researchLogger = getInstance(); researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_PICKSUGGESTIONMANUALLY, scrubDigitsFromString(replacedWord), index, suggestion == null ? null : scrubbedWord, Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE); - researchLogger.commitCurrentLogUnitAsWord(scrubbedWord, Long.MAX_VALUE); - researchLogger.mStatistics.recordManualSuggestion(); + researchLogger.commitCurrentLogUnitAsWord(scrubbedWord, Long.MAX_VALUE, isBatchMode); + researchLogger.mStatistics.recordManualSuggestion(SystemClock.uptimeMillis()); } /** @@ -1092,20 +1181,21 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang */ private static final LogStatement LOGSTATEMENT_LATINIME_PUNCTUATIONSUGGESTION = new LogStatement("LatinIMEPunctuationSuggestion", false, false, "index", "suggestion", - "x", "y"); - public static void latinIME_punctuationSuggestion(final int index, final String suggestion) { + "x", "y", "isPrediction"); + public static void latinIME_punctuationSuggestion(final int index, final String suggestion, + final boolean isBatchMode, final boolean isPrediction) { final ResearchLogger researchLogger = getInstance(); researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_PUNCTUATIONSUGGESTION, index, suggestion, - Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE); - researchLogger.commitCurrentLogUnitAsWord(suggestion, Long.MAX_VALUE); + Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE, + isPrediction); + researchLogger.commitCurrentLogUnitAsWord(suggestion, Long.MAX_VALUE, isBatchMode); } /** * Log a call to LatinIME.sendKeyCodePoint(). * - * SystemResponse: The IME is simulating a hardware keypress. This happens for numbers; other - * input typically goes through RichInputConnection.setComposingText() and - * RichInputConnection.commitText(). + * SystemResponse: The IME is inserting text into the TextView for numbers, fixed strings, or + * some other unusual mechanism. */ private static final LogStatement LOGSTATEMENT_LATINIME_SENDKEYCODEPOINT = new LogStatement("LatinIMESendKeyCodePoint", true, false, "code"); @@ -1119,17 +1209,45 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } /** + * Log a call to LatinIME.promotePhantomSpace(). + * + * SystemResponse: The IME is inserting a real space in place of a phantom space. + */ + private static final LogStatement LOGSTATEMENT_LATINIME_PROMOTEPHANTOMSPACE = + new LogStatement("LatinIMEPromotPhantomSpace", false, false); + public static void latinIME_promotePhantomSpace() { + final ResearchLogger researchLogger = getInstance(); + final LogUnit logUnit; + if (researchLogger.mMainLogBuffer == null) { + logUnit = researchLogger.mCurrentLogUnit; + } else { + logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit(); + } + researchLogger.enqueueEvent(logUnit, LOGSTATEMENT_LATINIME_PROMOTEPHANTOMSPACE); + } + + /** * Log a call to LatinIME.swapSwapperAndSpace(). * * SystemResponse: A symbol has been swapped with a space character. E.g. punctuation may swap * if a soft space is inserted after a word. */ private static final LogStatement LOGSTATEMENT_LATINIME_SWAPSWAPPERANDSPACE = - new LogStatement("LatinIMESwapSwapperAndSpace", false, false); - public static void latinIME_swapSwapperAndSpace(final String text) { + new LogStatement("LatinIMESwapSwapperAndSpace", false, false, "originalCharacters", + "charactersAfterSwap"); + public static void latinIME_swapSwapperAndSpace(final CharSequence originalCharacters, + final String charactersAfterSwap) { final ResearchLogger researchLogger = getInstance(); - researchLogger.commitCurrentLogUnitAsWord(text, Long.MAX_VALUE); - researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_SWAPSWAPPERANDSPACE); + final LogUnit logUnit; + if (researchLogger.mMainLogBuffer == null) { + logUnit = null; + } else { + logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit(); + } + if (logUnit != null) { + researchLogger.enqueueEvent(logUnit, LOGSTATEMENT_LATINIME_SWAPSWAPPERANDSPACE, + originalCharacters, charactersAfterSwap); + } } /** @@ -1137,9 +1255,10 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang * * SystemResponse: Two spaces have been replaced by period space. */ - public static void latinIME_maybeDoubleSpacePeriod(final String text) { + public static void latinIME_maybeDoubleSpacePeriod(final String text, + final boolean isBatchMode) { final ResearchLogger researchLogger = getInstance(); - researchLogger.commitCurrentLogUnitAsWord(text, Long.MAX_VALUE); + researchLogger.commitCurrentLogUnitAsWord(text, Long.MAX_VALUE, isBatchMode); } /** @@ -1169,8 +1288,9 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang public static void mainKeyboardView_setKeyboard(final Keyboard keyboard) { final KeyboardId kid = keyboard.mId; final boolean isPasswordView = kid.passwordInput(); - getInstance().setIsPasswordView(isPasswordView); - getInstance().enqueueEvent(LOGSTATEMENT_MAINKEYBOARDVIEW_SETKEYBOARD, + final ResearchLogger researchLogger = getInstance(); + researchLogger.setIsPasswordView(isPasswordView); + researchLogger.enqueueEvent(LOGSTATEMENT_MAINKEYBOARDVIEW_SETKEYBOARD, KeyboardId.elementIdToName(kid.mElementId), kid.mLocale + ":" + kid.mSubtype.getExtraValueOf(KEYBOARD_LAYOUT_SET), kid.mOrientation, kid.mWidth, KeyboardId.modeName(kid.mMode), kid.imeAction(), @@ -1189,14 +1309,28 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang */ private static final LogStatement LOGSTATEMENT_LATINIME_REVERTCOMMIT = new LogStatement("LatinIMERevertCommit", true, false, "committedWord", - "originallyTypedWord"); + "originallyTypedWord", "separatorString"); public static void latinIME_revertCommit(final String committedWord, - final String originallyTypedWord) { + final String originallyTypedWord, final boolean isBatchMode, + final String separatorString) { final ResearchLogger researchLogger = getInstance(); - researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_REVERTCOMMIT, committedWord, - originallyTypedWord); - researchLogger.mStatistics.recordRevertCommit(); - researchLogger.commitCurrentLogUnitAsWord(originallyTypedWord, Long.MAX_VALUE); + // TODO: Verify that mCurrentLogUnit has been restored and contains the reverted word. + final LogUnit logUnit; + if (researchLogger.mMainLogBuffer == null) { + logUnit = null; + } else { + logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit(); + } + if (originallyTypedWord.length() > 0 && hasLetters(originallyTypedWord)) { + if (logUnit != null) { + logUnit.setWord(originallyTypedWord); + } + } + researchLogger.enqueueEvent(logUnit != null ? logUnit : researchLogger.mCurrentLogUnit, + LOGSTATEMENT_LATINIME_REVERTCOMMIT, committedWord, originallyTypedWord, + separatorString); + researchLogger.mStatistics.recordRevertCommit(SystemClock.uptimeMillis()); + researchLogger.commitCurrentLogUnitAsWord(originallyTypedWord, Long.MAX_VALUE, isBatchMode); } /** @@ -1295,9 +1429,10 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang * * SystemResponse: The IME has reverted ". ", which had previously replaced two typed spaces. */ - public static void richInputConnection_revertDoubleSpacePeriod(final String doubleSpace) { - final ResearchLogger researchLogger = getInstance(); - researchLogger.commitCurrentLogUnitAsWord(doubleSpace, Long.MAX_VALUE); + private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_REVERTDOUBLESPACEPERIOD = + new LogStatement("RichInputConnectionRevertDoubleSpacePeriod", false, false); + public static void richInputConnection_revertDoubleSpacePeriod() { + getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_REVERTDOUBLESPACEPERIOD); } /** @@ -1305,9 +1440,10 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang * * SystemResponse: The IME has reverted a punctuation swap. */ - public static void richInputConnection_revertSwapPunctuation(final String text) { - final ResearchLogger researchLogger = getInstance(); - researchLogger.commitCurrentLogUnitAsWord(text, Long.MAX_VALUE); + private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_REVERTSWAPPUNCTUATION = + new LogStatement("RichInputConnectionRevertSwapPunctuation", false, false); + public static void richInputConnection_revertSwapPunctuation() { + getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_REVERTSWAPPUNCTUATION); } /** @@ -1317,16 +1453,24 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang * text input to another word that the user more likely desired to type. */ private static final LogStatement LOGSTATEMENT_LATINIME_COMMITCURRENTAUTOCORRECTION = - new LogStatement("LatinIMECommitCurrentAutoCorrection", true, false, "typedWord", + new LogStatement("LatinIMECommitCurrentAutoCorrection", true, true, "typedWord", "autoCorrection", "separatorString"); public static void latinIme_commitCurrentAutoCorrection(final String typedWord, - final String autoCorrection, final String separatorString) { + final String autoCorrection, final String separatorString, final boolean isBatchMode) { final String scrubbedTypedWord = scrubDigitsFromString(typedWord); final String scrubbedAutoCorrection = scrubDigitsFromString(autoCorrection); final ResearchLogger researchLogger = getInstance(); - researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_COMMITCURRENTAUTOCORRECTION, + researchLogger.commitCurrentLogUnitAsWord(scrubbedAutoCorrection, Long.MAX_VALUE, + isBatchMode); + + // Add the autocorrection logStatement at the end of the logUnit for the committed word. + // We have to do this after calling commitCurrentLogUnitAsWord, because it may split the + // current logUnit, and then we have to peek to get the logUnit reference back. + final LogUnit logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit(); + // TODO: Add test to confirm that the commitCurrentAutoCorrection log statement should + // always be added to logUnit (if non-null) and not mCurrentLogUnit. + researchLogger.enqueueEvent(logUnit, LOGSTATEMENT_LATINIME_COMMITCURRENTAUTOCORRECTION, scrubbedTypedWord, scrubbedAutoCorrection, separatorString); - researchLogger.commitCurrentLogUnitAsWord(scrubbedAutoCorrection, Long.MAX_VALUE); } private boolean isExpectingCommitText = false; @@ -1340,13 +1484,13 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang // add invocations. private static final LogStatement LOGSTATEMENT_LATINIME_COMMIT_PARTIAL_TEXT = new LogStatement("LatinIMECommitPartialText", true, false, "newCursorPosition"); - public static void latinIME_commitPartialText(final CharSequence committedWord, - final long lastTimestampOfWordData) { + public static void latinIME_commitPartialText(final String committedWord, + final long lastTimestampOfWordData, final boolean isBatchMode) { final ResearchLogger researchLogger = getInstance(); - final String scrubbedWord = scrubDigitsFromString(committedWord.toString()); + final String scrubbedWord = scrubDigitsFromString(committedWord); researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_COMMIT_PARTIAL_TEXT); - researchLogger.commitCurrentLogUnitAsWord(scrubbedWord, lastTimestampOfWordData); - researchLogger.mStatistics.recordSplitWords(); + researchLogger.commitCurrentLogUnitAsWord(scrubbedWord, lastTimestampOfWordData, + isBatchMode); } /** @@ -1357,14 +1501,14 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang */ private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTIONCOMMITTEXT = new LogStatement("RichInputConnectionCommitText", true, false, "newCursorPosition"); - public static void richInputConnection_commitText(final CharSequence committedWord, - final int newCursorPosition) { + public static void richInputConnection_commitText(final String committedWord, + final int newCursorPosition, final boolean isBatchMode) { final ResearchLogger researchLogger = getInstance(); - final String scrubbedWord = scrubDigitsFromString(committedWord.toString()); + final String scrubbedWord = scrubDigitsFromString(committedWord); if (!researchLogger.isExpectingCommitText) { researchLogger.enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTIONCOMMITTEXT, newCursorPosition); - researchLogger.commitCurrentLogUnitAsWord(scrubbedWord, Long.MAX_VALUE); + researchLogger.commitCurrentLogUnitAsWord(scrubbedWord, Long.MAX_VALUE, isBatchMode); } researchLogger.isExpectingCommitText = false; } @@ -1373,9 +1517,9 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang * Shared event for logging committed text. */ private static final LogStatement LOGSTATEMENT_COMMITTEXT = - new LogStatement("CommitText", true, false, "committedText"); - private void enqueueCommitText(final CharSequence word) { - enqueueEvent(LOGSTATEMENT_COMMITTEXT, word); + new LogStatement("CommitText", true, false, "committedText", "isBatchMode"); + private void enqueueCommitText(final String word, final boolean isBatchMode) { + enqueueEvent(LOGSTATEMENT_COMMITTEXT, word, isBatchMode); } /** @@ -1515,20 +1659,62 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang final ResearchLogger researchLogger = getInstance(); researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_ONENDBATCHINPUT, enteredText, enteredWordPos); - researchLogger.mStatistics.recordGestureInput(enteredText.length()); + researchLogger.mStatistics.recordGestureInput(enteredText.length(), + SystemClock.uptimeMillis()); } /** - * Log a call to LatinIME.handleBackspace(). + * Log a call to LatinIME.handleBackspace() that is not a batch delete. + * + * UserInput: The user is deleting one or more characters by hitting the backspace key once. + * The covers single character deletes as well as deleting selections. + */ + private static final LogStatement LOGSTATEMENT_LATINIME_HANDLEBACKSPACE = + new LogStatement("LatinIMEHandleBackspace", true, false, "numCharacters"); + public static void latinIME_handleBackspace(final int numCharacters) { + final ResearchLogger researchLogger = getInstance(); + researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_HANDLEBACKSPACE, numCharacters); + } + + /** + * Log a call to LatinIME.handleBackspace() that is a batch delete. * * UserInput: The user is deleting a gestured word by hitting the backspace key once. */ private static final LogStatement LOGSTATEMENT_LATINIME_HANDLEBACKSPACE_BATCH = - new LogStatement("LatinIMEHandleBackspaceBatch", true, false, "deletedText"); - public static void latinIME_handleBackspace_batch(final CharSequence deletedText) { + new LogStatement("LatinIMEHandleBackspaceBatch", true, false, "deletedText", + "numCharacters"); + public static void latinIME_handleBackspace_batch(final CharSequence deletedText, + final int numCharacters) { + final ResearchLogger researchLogger = getInstance(); + researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_HANDLEBACKSPACE_BATCH, deletedText, + numCharacters); + researchLogger.mStatistics.recordGestureDelete(deletedText.length(), + SystemClock.uptimeMillis()); + } + + /** + * Log a long interval between user operation. + * + * UserInput: The user has not done anything for a while. + */ + private static final LogStatement LOGSTATEMENT_ONUSERPAUSE = new LogStatement("OnUserPause", + false, false, "intervalInMs"); + public static void onUserPause(final long interval) { + final ResearchLogger researchLogger = getInstance(); + researchLogger.enqueueEvent(LOGSTATEMENT_ONUSERPAUSE, interval); + } + + /** + * Record the current time in case the LogUnit is later split. + * + * If the current logUnitis split, then tapping, motion events, etc. before this time should + * be assigned to one LogUnit, and events after this time should go into the following LogUnit. + */ + public static void recordTimeForLogUnitSplit() { final ResearchLogger researchLogger = getInstance(); - researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_HANDLEBACKSPACE_BATCH, deletedText); - researchLogger.mStatistics.recordGestureDelete(); + researchLogger.setSavedDownEventTime(SystemClock.uptimeMillis()); + researchLogger.mSavedDownEventTime = Long.MAX_VALUE; } /** diff --git a/java/src/com/android/inputmethod/research/Statistics.java b/java/src/com/android/inputmethod/research/Statistics.java index e9c02c919..a9202651e 100644 --- a/java/src/com/android/inputmethod/research/Statistics.java +++ b/java/src/com/android/inputmethod/research/Statistics.java @@ -134,17 +134,9 @@ public class Statistics { if (DEBUG) { Log.d(TAG, "recordChar() called"); } - final long delta = time - mLastTapTime; if (codePoint == Constants.CODE_DELETE) { mDeleteKeyCount++; - if (delta < MIN_DELETION_INTERMISSION) { - if (mIsLastKeyDeleteKey) { - mDuringRepeatedDeleteKeysCounter.add(delta); - } else { - mBeforeDeleteKeyCounter.add(delta); - } - } - mIsLastKeyDeleteKey = true; + recordUserAction(time, true /* isDeletion */); } else { mCharCount++; if (Character.isDigit(codePoint)) { @@ -156,14 +148,8 @@ public class Statistics { 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; + recordUserAction(time, false /* isDeletion */); } - mLastTapTime = time; } public void recordWordEntered(final boolean isDictionaryWord) { @@ -177,9 +163,10 @@ public class Statistics { mSplitWordsCount++; } - public void recordGestureInput(final int numCharsEntered) { + public void recordGestureInput(final int numCharsEntered, final long time) { mGesturesInputCount++; mGesturesCharsCount += numCharsEntered; + recordUserAction(time, false /* isDeletion */); } public void setIsEmptyUponStarting(final boolean isEmpty) { @@ -187,14 +174,43 @@ public class Statistics { mIsEmptinessStateKnown = true; } - public void recordGestureDelete() { + public void recordGestureDelete(final int length, final long time) { mGesturesDeletedCount++; + recordUserAction(time, true /* isDeletion */); } - public void recordManualSuggestion() { + + public void recordManualSuggestion(final long time) { mManualSuggestionsCount++; + recordUserAction(time, false /* isDeletion */); } - public void recordRevertCommit() { + public void recordRevertCommit(final long time) { mRevertCommitsCount++; + recordUserAction(time, true /* isDeletion */); + } + + private void recordUserAction(final long time, final boolean isDeletion) { + final long delta = time - mLastTapTime; + if (isDeletion) { + if (delta < MIN_DELETION_INTERMISSION) { + if (mIsLastKeyDeleteKey) { + mDuringRepeatedDeleteKeysCounter.add(delta); + } else { + mBeforeDeleteKeyCounter.add(delta); + } + } else { + ResearchLogger.onUserPause(delta); + } + } else { + if (mIsLastKeyDeleteKey && delta < MIN_DELETION_INTERMISSION) { + mAfterDeleteKeyCounter.add(delta); + } else if (!mIsLastKeyDeleteKey && delta < MIN_TYPING_INTERMISSION) { + mKeyCounter.add(delta); + } else { + ResearchLogger.onUserPause(delta); + } + } + mIsLastKeyDeleteKey = isDeletion; + mLastTapTime = time; } } |