diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml
index f2468f5a4..07b3f31c7 100644
--- a/java/res/values/strings.xml
+++ b/java/res/values/strings.xml
@@ -261,8 +261,7 @@
Send feedback
-
- Include last %d words entered
+ Include last 5 words entered
Enter your feedback here.
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index ea5cf2ae4..aa2f185e8 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -1247,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
@@ -1328,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
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..b5bdc1958 100644
--- a/java/src/com/android/inputmethod/research/ResearchLogger.java
+++ b/java/src/com/android/inputmethod/research/ResearchLogger.java
@@ -67,8 +67,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 +97,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;
@@ -133,9 +139,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
private KeyboardSwitcher mKeyboardSwitcher;
private InputMethodService mInputMethodService;
private final Statistics mStatistics;
- private ResearchLogUploader mResearchLogUploader;
- private LogUnit mCurrentLogUnit = new LogUnit();
+ private ResearchLogUploader mResearchLogUploader;
private ResearchLogger() {
mStatistics = Statistics.getInstance();
@@ -259,16 +264,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
e.apply();
}
- private void setLoggingAllowed(boolean enableLogging) {
- if (mPrefs == null) {
- return;
- }
- Editor e = mPrefs.edit();
- e.putBoolean(PREF_USABILITY_STUDY_MODE, enableLogging);
- e.apply();
- sIsLogging = enableLogging;
- }
-
private File createLogFile(File filesDir) {
final StringBuilder sb = new StringBuilder();
sb.append(FILENAME_PREFIX).append('-');
@@ -315,58 +310,97 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
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);
+ 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() {
logStatistics();
- commitCurrentLogUnit();
+ publishLogUnit(mCurrentLogUnit, true);
+ mCurrentLogUnit = new LogUnit();
- if (mMainLogBuffer != null) {
- publishLogBuffer(mMainLogBuffer, mMainResearchLog, false /* isIncludingPrivateData */);
- mMainResearchLog.close();
- mMainLogBuffer = null;
+ if (mMainResearchLog != null) {
+ mMainResearchLog.stop();
}
- if (mFeedbackLogBuffer != null) {
- mFeedbackLog.close();
- mFeedbackLogBuffer = null;
+ if (mIntentionalResearchLog != null) {
+ mIntentionalResearchLog.stop();
}
}
+ private static final String[] EVENTKEYS_STATISTICS = {
+ "Statistics", "charCount", "letterCount", "numberCount", "spaceCount", "deleteOpsCount",
+ "wordCount", "isEmptyUponStarting", "isEmptinessStateKnown", "averageTimeBetweenKeys",
+ "averageTimeBeforeDelete", "averageTimeDuringRepeatedDelete", "averageTimeAfterDelete"
+ };
+ private static void logStatistics() {
+ final ResearchLogger researchLogger = getInstance();
+ final Statistics statistics = researchLogger.mStatistics;
+ final Object[] values = {
+ statistics.mCharCount, statistics.mLetterCount, statistics.mNumberCount,
+ statistics.mSpaceCount, statistics.mDeleteKeyCount,
+ statistics.mWordCount, statistics.mIsEmptyUponStarting,
+ statistics.mIsEmptinessStateKnown, statistics.mKeyCounter.getAverageTime(),
+ statistics.mBeforeDeleteKeyCounter.getAverageTime(),
+ statistics.mDuringRepeatedDeleteKeysCounter.getAverageTime(),
+ statistics.mAfterDeleteKeyCounter.getAverageTime()
+ };
+ researchLogger.enqueueEvent(EVENTKEYS_STATISTICS, values);
+ }
+
+ private void setLoggingAllowed(boolean enableLogging) {
+ if (mPrefs == null) {
+ return;
+ }
+ Editor e = mPrefs.edit();
+ e.putBoolean(PREF_USABILITY_STUDY_MODE, enableLogging);
+ e.apply();
+ sIsLogging = enableLogging;
+ }
+
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 +504,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 +584,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
}
private boolean isAllowedToLog() {
- return !mIsPasswordView && !mIsLoggingSuspended && sIsLogging && !mInFeedbackDialog;
+ return !mIsPasswordView && !mIsLoggingSuspended && sIsLogging;
}
public void requestIndicatorRedraw() {
@@ -557,8 +631,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 +653,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 +672,140 @@ 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();
+ mStatistics.recordWordEntered();
+ }
+
+ 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 +858,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 +871,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;
@@ -735,6 +905,16 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
"UserFeedback", "FeedbackContents"
};
+ private void userFeedback(ResearchLog researchLog, String feedbackContents) {
+ // this method is special; it directs the feedbackContents to a particular researchLog
+ final LogUnit logUnit = new LogUnit();
+ final Object[] values = {
+ feedbackContents
+ };
+ logUnit.addLogAtom(EVENTKEYS_USER_FEEDBACK, values, false);
+ researchLog.publishAllEvents(logUnit);
+ }
+
// Regular logging methods
private static final String[] EVENTKEYS_MAINKEYBOARDVIEW_PROCESSMOTIONEVENT = {
@@ -769,16 +949,12 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
"LatinIMEOnCodeInput", "code", "x", "y"
};
public static void latinIME_onCodeInput(final int code, final int x, final int y) {
- final long time = SystemClock.uptimeMillis();
- final ResearchLogger researchLogger = getInstance();
final Object[] values = {
Keyboard.printableCode(scrubDigitFromCodePoint(code)), x, y
};
+ final ResearchLogger researchLogger = getInstance();
researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_ONCODEINPUT, values);
- if (Character.isDigit(code)) {
- researchLogger.setCurrentLogUnitContainsDigitFlag();
- }
- researchLogger.mStatistics.recordChar(code, time);
+ researchLogger.mStatistics.recordChar(code, SystemClock.uptimeMillis());
}
private static final String[] EVENTKEYS_LATINIME_ONDISPLAYCOMPLETIONS = {
@@ -843,7 +1019,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();
}
}
@@ -908,11 +1086,7 @@ 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 = {
@@ -1050,21 +1224,10 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
EVENTKEYS_RICHINPUTCONNECTION_COMMITCOMPLETION, values);
}
- // Disabled for privacy-protection reasons. Because this event comes after
- // richInputConnection_commitText, which is the event used to separate LogUnits, the
- // data in this event can be associated with the next LogUnit, revealing information
- // about the current word even if it was supposed to be suppressed. The occurrance of
- // autocorrection can be determined by examining the difference between the text strings in
- // the last call to richInputConnection_setComposingText before
- // richInputConnection_commitText, so it's not a data loss.
- // TODO: Figure out how to log this event without loss of privacy.
- /*
private static final String[] EVENTKEYS_RICHINPUTCONNECTION_COMMITCORRECTION = {
- "RichInputConnectionCommitCorrection", "typedWord", "autoCorrection"
+ "RichInputConnectionCommitCorrection", "CorrectionInfo"
};
- */
public static void richInputConnection_commitCorrection(CorrectionInfo correctionInfo) {
- /*
final String typedWord = correctionInfo.getOldText().toString();
final String autoCorrection = correctionInfo.getNewText().toString();
final Object[] values = {
@@ -1073,7 +1236,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
final ResearchLogger researchLogger = getInstance();
researchLogger.enqueuePotentiallyPrivateEvent(
EVENTKEYS_RICHINPUTCONNECTION_COMMITCORRECTION, values);
- */
}
private static final String[] EVENTKEYS_RICHINPUTCONNECTION_COMMITTEXT = {
@@ -1099,8 +1261,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
final Object[] values = {
beforeLength, afterLength
};
- getInstance().enqueuePotentiallyPrivateEvent(
- EVENTKEYS_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT, values);
+ getInstance().enqueueEvent(EVENTKEYS_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT, values);
}
private static final String[] EVENTKEYS_RICHINPUTCONNECTION_FINISHCOMPOSINGTEXT = {
@@ -1130,8 +1291,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
keyEvent.getAction(),
keyEvent.getKeyCode()
};
- getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_RICHINPUTCONNECTION_SENDKEYEVENT,
- values);
+ getInstance().enqueueEvent(EVENTKEYS_RICHINPUTCONNECTION_SENDKEYEVENT, values);
}
private static final String[] EVENTKEYS_RICHINPUTCONNECTION_SETCOMPOSINGTEXT = {
@@ -1139,14 +1299,10 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
};
public static void richInputConnection_setComposingText(final CharSequence text,
final int newCursorPosition) {
- if (text == null) {
- throw new RuntimeException("setComposingText is null");
- }
final Object[] values = {
text, newCursorPosition
};
- getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_RICHINPUTCONNECTION_SETCOMPOSINGTEXT,
- values);
+ getInstance().enqueueEvent(EVENTKEYS_RICHINPUTCONNECTION_SETCOMPOSINGTEXT, values);
}
private static final String[] EVENTKEYS_RICHINPUTCONNECTION_SETSELECTION = {
@@ -1156,8 +1312,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
final Object[] values = {
from, to
};
- getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_RICHINPUTCONNECTION_SETSELECTION,
- values);
+ getInstance().enqueueEvent(EVENTKEYS_RICHINPUTCONNECTION_SETSELECTION, values);
}
private static final String[] EVENTKEYS_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT = {
@@ -1192,24 +1347,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
index eab465aa2..4a2cd079c 100644
--- a/java/src/com/android/inputmethod/research/Statistics.java
+++ b/java/src/com/android/inputmethod/research/Statistics.java
@@ -66,8 +66,8 @@ public class Statistics {
// To account for the interruptions when the user's attention is directed elsewhere, times
// longer than MIN_TYPING_INTERMISSION are not counted when estimating this statistic.
- public static final int MIN_TYPING_INTERMISSION = 2 * 1000; // in milliseconds
- public static final int MIN_DELETION_INTERMISSION = 10 * 1000; // in milliseconds
+ public static final int MIN_TYPING_INTERMISSION = 5 * 1000; // in milliseconds
+ public static final int MIN_DELETION_INTERMISSION = 15 * 1000; // in milliseconds
// The last time that a tap was performed
private long mLastTapTime;