Merge changes I568c90d4,I4667f8f6 into jb-mr1-dev
* changes: ResearchLogging capture full n-gram data ResearchLogger to track simple statisticsmain
commit
752c53760c
|
@ -261,7 +261,8 @@
|
||||||
<string name="research_feedback_dialog_title" translatable="false">Send feedback</string>
|
<string name="research_feedback_dialog_title" translatable="false">Send feedback</string>
|
||||||
<!-- Text for checkbox option to include user data in feedback for research purposes [CHAR LIMIT=50] -->
|
<!-- Text for checkbox option to include user data in feedback for research purposes [CHAR LIMIT=50] -->
|
||||||
<!-- TODO: remove translatable=false attribute once text is stable -->
|
<!-- TODO: remove translatable=false attribute once text is stable -->
|
||||||
<string name="research_feedback_include_history_label" translatable="false">Include last 5 words entered</string>
|
<!-- TODO: handle multilingual plurals -->
|
||||||
|
<string name="research_feedback_include_history_label" translatable="false">Include last <xliff:g id="word">%d</xliff:g> words entered</string>
|
||||||
<!-- Hint to user about the text entry field where they should enter research feedback [CHAR LIMIT=40] -->
|
<!-- Hint to user about the text entry field where they should enter research feedback [CHAR LIMIT=40] -->
|
||||||
<!-- TODO: remove translatable=false attribute once text is stable -->
|
<!-- TODO: remove translatable=false attribute once text is stable -->
|
||||||
<string name="research_feedback_hint" translatable="false">Enter your feedback here.</string>
|
<string name="research_feedback_hint" translatable="false">Enter your feedback here.</string>
|
||||||
|
|
|
@ -1247,11 +1247,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
|
||||||
}
|
}
|
||||||
mLastKeyTime = when;
|
mLastKeyTime = when;
|
||||||
mConnection.beginBatchEdit();
|
mConnection.beginBatchEdit();
|
||||||
|
|
||||||
if (ProductionFlag.IS_EXPERIMENTAL) {
|
|
||||||
ResearchLogger.latinIME_onCodeInput(primaryCode, x, y);
|
|
||||||
}
|
|
||||||
|
|
||||||
final KeyboardSwitcher switcher = mKeyboardSwitcher;
|
final KeyboardSwitcher switcher = mKeyboardSwitcher;
|
||||||
// The space state depends only on the last character pressed and its own previous
|
// 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
|
// state. Here, we revert the space state to neutral if the key is actually modifying
|
||||||
|
@ -1333,6 +1328,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
|
||||||
mLastComposedWord.deactivate();
|
mLastComposedWord.deactivate();
|
||||||
mEnteredText = null;
|
mEnteredText = null;
|
||||||
mConnection.endBatchEdit();
|
mConnection.endBatchEdit();
|
||||||
|
if (ProductionFlag.IS_EXPERIMENTAL) {
|
||||||
|
ResearchLogger.latinIME_onCodeInput(primaryCode, x, y);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called from PointerTracker through the KeyboardActionListener interface
|
// Called from PointerTracker through the KeyboardActionListener interface
|
||||||
|
|
|
@ -18,10 +18,7 @@ package com.android.inputmethod.research;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.Editable;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.CheckBox;
|
import android.widget.CheckBox;
|
||||||
import android.widget.EditText;
|
|
||||||
|
|
||||||
import com.android.inputmethod.latin.R;
|
import com.android.inputmethod.latin.R;
|
||||||
|
|
||||||
|
@ -31,6 +28,11 @@ public class FeedbackActivity extends Activity {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.research_feedback_activity);
|
setContentView(R.layout.research_feedback_activity);
|
||||||
final FeedbackLayout layout = (FeedbackLayout) findViewById(R.id.research_feedback_layout);
|
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);
|
layout.setActivity(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,111 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2012 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy of
|
||||||
|
* the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.inputmethod.research;
|
||||||
|
|
||||||
|
import java.util.LinkedList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A buffer that holds a fixed number of LogUnits.
|
||||||
|
*
|
||||||
|
* LogUnits are added in and shifted out in temporal order. Only a subset of the LogUnits are
|
||||||
|
* actual words; the other LogUnits do not count toward the word limit. Once the buffer reaches
|
||||||
|
* capacity, adding another LogUnit that is a word evicts the oldest LogUnits out one at a time to
|
||||||
|
* stay under the capacity limit.
|
||||||
|
*/
|
||||||
|
public class LogBuffer {
|
||||||
|
protected final LinkedList<LogUnit> mLogUnits;
|
||||||
|
/* package for test */ int mWordCapacity;
|
||||||
|
// The number of members of mLogUnits that are actual words.
|
||||||
|
protected int mNumActualWords;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new LogBuffer that can hold a fixed number of LogUnits that are words (and
|
||||||
|
* unlimited number of non-word LogUnits), and that outputs its result to a researchLog.
|
||||||
|
*
|
||||||
|
* @param wordCapacity maximum number of words
|
||||||
|
*/
|
||||||
|
LogBuffer(final int wordCapacity) {
|
||||||
|
if (wordCapacity <= 0) {
|
||||||
|
throw new IllegalArgumentException("wordCapacity must be 1 or greater.");
|
||||||
|
}
|
||||||
|
mLogUnits = new LinkedList<LogUnit>();
|
||||||
|
mWordCapacity = wordCapacity;
|
||||||
|
mNumActualWords = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new LogUnit to the front of the LIFO queue, evicting existing LogUnit's
|
||||||
|
* (oldest first) if word capacity is reached.
|
||||||
|
*/
|
||||||
|
public void shiftIn(LogUnit newLogUnit) {
|
||||||
|
if (newLogUnit.getWord() == null) {
|
||||||
|
// This LogUnit isn't a word, so it doesn't count toward the word-limit.
|
||||||
|
mLogUnits.add(newLogUnit);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mNumActualWords == mWordCapacity) {
|
||||||
|
shiftOutThroughFirstWord();
|
||||||
|
}
|
||||||
|
mLogUnits.add(newLogUnit);
|
||||||
|
mNumActualWords++; // Must be a word, or we wouldn't be here.
|
||||||
|
}
|
||||||
|
|
||||||
|
private void shiftOutThroughFirstWord() {
|
||||||
|
while (!mLogUnits.isEmpty()) {
|
||||||
|
final LogUnit logUnit = mLogUnits.removeFirst();
|
||||||
|
onShiftOut(logUnit);
|
||||||
|
if (logUnit.hasWord()) {
|
||||||
|
// Successfully shifted out a word-containing LogUnit and made space for the new
|
||||||
|
// LogUnit.
|
||||||
|
mNumActualWords--;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all LogUnits from the buffer without calling onShiftOut().
|
||||||
|
*/
|
||||||
|
public void clear() {
|
||||||
|
mLogUnits.clear();
|
||||||
|
mNumActualWords = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a LogUnit is removed from the LogBuffer as a result of a shiftIn. LogUnits are
|
||||||
|
* removed in the order entered. This method is not called when shiftOut is called directly.
|
||||||
|
*
|
||||||
|
* Base class does nothing; subclasses may override.
|
||||||
|
*/
|
||||||
|
protected void onShiftOut(LogUnit logUnit) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called to deliberately remove the oldest LogUnit. Usually called when draining the
|
||||||
|
* LogBuffer.
|
||||||
|
*/
|
||||||
|
public LogUnit shiftOut() {
|
||||||
|
if (mLogUnits.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final LogUnit logUnit = mLogUnits.removeFirst();
|
||||||
|
if (logUnit.hasWord()) {
|
||||||
|
mNumActualWords--;
|
||||||
|
}
|
||||||
|
return logUnit;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2012 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy of
|
||||||
|
* the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.inputmethod.research;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A group of log statements related to each other.
|
||||||
|
*
|
||||||
|
* A LogUnit is collection of LogStatements, each of which is generated by at a particular point
|
||||||
|
* in the code. (There is no LogStatement class; the data is stored across the instance variables
|
||||||
|
* here.) A single LogUnit's statements can correspond to all the calls made while in the same
|
||||||
|
* composing region, or all the calls between committing the last composing region, and the first
|
||||||
|
* character of the next composing region.
|
||||||
|
*
|
||||||
|
* Individual statements in a log may be marked as potentially private. If so, then they are only
|
||||||
|
* published to a ResearchLog if the ResearchLogger determines that publishing the entire LogUnit
|
||||||
|
* will not violate the user's privacy. Checks for this may include whether other LogUnits have
|
||||||
|
* been published recently, or whether the LogUnit contains numbers, etc.
|
||||||
|
*/
|
||||||
|
/* package */ class LogUnit {
|
||||||
|
private final ArrayList<String[]> mKeysList = new ArrayList<String[]>();
|
||||||
|
private final ArrayList<Object[]> mValuesList = new ArrayList<Object[]>();
|
||||||
|
private final ArrayList<Boolean> mIsPotentiallyPrivate = new ArrayList<Boolean>();
|
||||||
|
private String mWord;
|
||||||
|
private boolean mContainsDigit;
|
||||||
|
|
||||||
|
public void addLogStatement(final String[] keys, final Object[] values,
|
||||||
|
final Boolean isPotentiallyPrivate) {
|
||||||
|
mKeysList.add(keys);
|
||||||
|
mValuesList.add(values);
|
||||||
|
mIsPotentiallyPrivate.add(isPotentiallyPrivate);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void publishTo(final ResearchLog researchLog, final boolean isIncludingPrivateData) {
|
||||||
|
final int size = mKeysList.size();
|
||||||
|
for (int i = 0; i < size; i++) {
|
||||||
|
if (!mIsPotentiallyPrivate.get(i) || isIncludingPrivateData) {
|
||||||
|
researchLog.outputEvent(mKeysList.get(i), mValuesList.get(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWord(String word) {
|
||||||
|
mWord = word;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getWord() {
|
||||||
|
return mWord;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasWord() {
|
||||||
|
return mWord != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContainsDigit() {
|
||||||
|
mContainsDigit = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasDigit() {
|
||||||
|
return mContainsDigit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEmpty() {
|
||||||
|
return mKeysList.isEmpty();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,127 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2012 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy of
|
||||||
|
* the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.inputmethod.research;
|
||||||
|
|
||||||
|
import com.android.inputmethod.latin.Dictionary;
|
||||||
|
import com.android.inputmethod.latin.Suggest;
|
||||||
|
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
public class MainLogBuffer extends LogBuffer {
|
||||||
|
// The size of the n-grams logged. E.g. N_GRAM_SIZE = 2 means to sample bigrams.
|
||||||
|
private static final int N_GRAM_SIZE = 2;
|
||||||
|
// The number of words between n-grams to omit from the log.
|
||||||
|
private static final int DEFAULT_NUMBER_OF_WORDS_BETWEEN_SAMPLES = 18;
|
||||||
|
|
||||||
|
private final ResearchLog mResearchLog;
|
||||||
|
private Suggest mSuggest;
|
||||||
|
|
||||||
|
// The minimum periodicity with which n-grams can be sampled. E.g. mWinWordPeriod is 10 if
|
||||||
|
// every 10th bigram is sampled, i.e., words 1-8 are not, but the bigram at words 9 and 10, etc.
|
||||||
|
// for 11-18, and the bigram at words 19 and 20. If an n-gram is not safe (e.g. it contains a
|
||||||
|
// number in the middle or an out-of-vocabulary word), then sampling is delayed until a safe
|
||||||
|
// n-gram does appear.
|
||||||
|
/* package for test */ int mMinWordPeriod;
|
||||||
|
|
||||||
|
// Counter for words left to suppress before an n-gram can be sampled. Reset to mMinWordPeriod
|
||||||
|
// after a sample is taken.
|
||||||
|
/* package for test */ int mWordsUntilSafeToSample;
|
||||||
|
|
||||||
|
public MainLogBuffer(final ResearchLog researchLog) {
|
||||||
|
super(N_GRAM_SIZE);
|
||||||
|
mResearchLog = researchLog;
|
||||||
|
mMinWordPeriod = DEFAULT_NUMBER_OF_WORDS_BETWEEN_SAMPLES + N_GRAM_SIZE;
|
||||||
|
final Random random = new Random();
|
||||||
|
mWordsUntilSafeToSample = random.nextInt(mMinWordPeriod);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSuggest(Suggest suggest) {
|
||||||
|
mSuggest = suggest;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void shiftIn(final LogUnit newLogUnit) {
|
||||||
|
super.shiftIn(newLogUnit);
|
||||||
|
if (newLogUnit.hasWord()) {
|
||||||
|
if (mWordsUntilSafeToSample > 0) {
|
||||||
|
mWordsUntilSafeToSample--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetWordCounter() {
|
||||||
|
mWordsUntilSafeToSample = mMinWordPeriod;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether the content of the MainLogBuffer can be safely uploaded in its complete
|
||||||
|
* form and still protect the user's privacy.
|
||||||
|
*
|
||||||
|
* The size of the MainLogBuffer is just enough to hold one n-gram, its corrections, and any
|
||||||
|
* non-character data that is typed between words. The decision about privacy is made based on
|
||||||
|
* the buffer's entire content. If it is decided that the privacy risks are too great to upload
|
||||||
|
* the contents of this buffer, a censored version of the LogItems may still be uploaded. E.g.,
|
||||||
|
* the screen orientation and other characteristics about the device can be uploaded without
|
||||||
|
* revealing much about the user.
|
||||||
|
*/
|
||||||
|
public boolean isSafeToLog() {
|
||||||
|
// Check that we are not sampling too frequently. Having sampled recently might disclose
|
||||||
|
// too much of the user's intended meaning.
|
||||||
|
if (mWordsUntilSafeToSample > 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (mSuggest == null || !mSuggest.hasMainDictionary()) {
|
||||||
|
// Main dictionary is unavailable. Since we cannot check it, we cannot tell if a word
|
||||||
|
// is out-of-vocabulary or not. Therefore, we must judge the entire buffer contents to
|
||||||
|
// potentially pose a privacy risk.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Reload the dictionary in case it has changed (e.g., because the user has changed
|
||||||
|
// languages).
|
||||||
|
final Dictionary dictionary = mSuggest.getMainDictionary();
|
||||||
|
if (dictionary == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Check each word in the buffer. If any word poses a privacy threat, we cannot upload the
|
||||||
|
// complete buffer contents in detail.
|
||||||
|
final int length = mLogUnits.size();
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
final LogUnit logUnit = mLogUnits.get(i);
|
||||||
|
final String word = logUnit.getWord();
|
||||||
|
if (word == null) {
|
||||||
|
// Digits outside words are a privacy threat.
|
||||||
|
if (logUnit.hasDigit()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Words not in the dictionary are a privacy threat.
|
||||||
|
if (!(dictionary.isValidWord(word))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// All checks have passed; this buffer's content can be safely uploaded.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onShiftOut(LogUnit logUnit) {
|
||||||
|
if (mResearchLog != null) {
|
||||||
|
mResearchLog.publish(logUnit, false /* isIncludingPrivateData */);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,7 +26,6 @@ import com.android.inputmethod.keyboard.Key;
|
||||||
import com.android.inputmethod.latin.SuggestedWords;
|
import com.android.inputmethod.latin.SuggestedWords;
|
||||||
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
|
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
|
||||||
import com.android.inputmethod.latin.define.ProductionFlag;
|
import com.android.inputmethod.latin.define.ProductionFlag;
|
||||||
import com.android.inputmethod.research.ResearchLogger.LogUnit;
|
|
||||||
|
|
||||||
import java.io.BufferedWriter;
|
import java.io.BufferedWriter;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
@ -37,6 +36,7 @@ import java.io.OutputStreamWriter;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.Callable;
|
import java.util.concurrent.Callable;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.RejectedExecutionException;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
import java.util.concurrent.ScheduledFuture;
|
import java.util.concurrent.ScheduledFuture;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
@ -51,21 +51,22 @@ import java.util.concurrent.TimeUnit;
|
||||||
*/
|
*/
|
||||||
public class ResearchLog {
|
public class ResearchLog {
|
||||||
private static final String TAG = ResearchLog.class.getSimpleName();
|
private static final String TAG = ResearchLog.class.getSimpleName();
|
||||||
private static final JsonWriter NULL_JSON_WRITER = new JsonWriter(
|
private static final boolean DEBUG = false;
|
||||||
new OutputStreamWriter(new NullOutputStream()));
|
private static final long FLUSH_DELAY_IN_MS = 1000 * 5;
|
||||||
|
private static final int ABORT_TIMEOUT_IN_MS = 1000 * 4;
|
||||||
|
|
||||||
final ScheduledExecutorService mExecutor;
|
/* package */ final ScheduledExecutorService mExecutor;
|
||||||
/* package */ final File mFile;
|
/* package */ final File mFile;
|
||||||
private JsonWriter mJsonWriter = NULL_JSON_WRITER;
|
private JsonWriter mJsonWriter = NULL_JSON_WRITER;
|
||||||
|
// true if at least one byte of data has been written out to the log file. This must be
|
||||||
|
// remembered because JsonWriter requires that calls matching calls to beginObject and
|
||||||
|
// endObject, as well as beginArray and endArray, and the file is opened lazily, only when
|
||||||
|
// it is certain that data will be written. Alternatively, the matching call exceptions
|
||||||
|
// could be caught, but this might suppress other errors.
|
||||||
|
private boolean mHasWrittenData = false;
|
||||||
|
|
||||||
private int mLoggingState;
|
private static final JsonWriter NULL_JSON_WRITER = new JsonWriter(
|
||||||
private static final int LOGGING_STATE_UNSTARTED = 0;
|
new OutputStreamWriter(new NullOutputStream()));
|
||||||
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 {
|
private static class NullOutputStream extends OutputStream {
|
||||||
/** {@inheritDoc} */
|
/** {@inheritDoc} */
|
||||||
@Override
|
@Override
|
||||||
|
@ -84,128 +85,81 @@ public class ResearchLog {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ResearchLog(File outputFile) {
|
public ResearchLog(final File outputFile) {
|
||||||
mExecutor = Executors.newSingleThreadScheduledExecutor();
|
|
||||||
if (outputFile == null) {
|
if (outputFile == null) {
|
||||||
throw new IllegalArgumentException();
|
throw new IllegalArgumentException();
|
||||||
}
|
}
|
||||||
|
mExecutor = Executors.newSingleThreadScheduledExecutor();
|
||||||
mFile = outputFile;
|
mFile = outputFile;
|
||||||
mLoggingState = LOGGING_STATE_UNSTARTED;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void start() throws IOException {
|
public synchronized void close() {
|
||||||
switch (mLoggingState) {
|
|
||||||
case LOGGING_STATE_UNSTARTED:
|
|
||||||
mLoggingState = LOGGING_STATE_READY;
|
|
||||||
break;
|
|
||||||
case LOGGING_STATE_READY:
|
|
||||||
case LOGGING_STATE_RUNNING:
|
|
||||||
case LOGGING_STATE_STOPPING:
|
|
||||||
case LOGGING_STATE_STOPPED:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized void stop() {
|
|
||||||
switch (mLoggingState) {
|
|
||||||
case LOGGING_STATE_UNSTARTED:
|
|
||||||
mLoggingState = LOGGING_STATE_STOPPED;
|
|
||||||
break;
|
|
||||||
case LOGGING_STATE_READY:
|
|
||||||
case LOGGING_STATE_RUNNING:
|
|
||||||
mExecutor.submit(new Callable<Object>() {
|
mExecutor.submit(new Callable<Object>() {
|
||||||
@Override
|
@Override
|
||||||
public Object call() throws Exception {
|
public Object call() throws Exception {
|
||||||
try {
|
try {
|
||||||
|
if (mHasWrittenData) {
|
||||||
mJsonWriter.endArray();
|
mJsonWriter.endArray();
|
||||||
mJsonWriter.flush();
|
mJsonWriter.flush();
|
||||||
mJsonWriter.close();
|
mJsonWriter.close();
|
||||||
|
mHasWrittenData = false;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.d(TAG, "error when closing ResearchLog:");
|
||||||
|
e.printStackTrace();
|
||||||
} finally {
|
} finally {
|
||||||
boolean success = mFile.setWritable(false, false);
|
if (mFile.exists()) {
|
||||||
mLoggingState = LOGGING_STATE_STOPPED;
|
mFile.setWritable(false, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
removeAnyScheduledFlush();
|
removeAnyScheduledFlush();
|
||||||
mExecutor.shutdown();
|
mExecutor.shutdown();
|
||||||
mLoggingState = LOGGING_STATE_STOPPING;
|
|
||||||
break;
|
|
||||||
case LOGGING_STATE_STOPPING:
|
|
||||||
case LOGGING_STATE_STOPPED:
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isAlive() {
|
private boolean mIsAbortSuccessful;
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized void abort() {
|
public synchronized void abort() {
|
||||||
switch (mLoggingState) {
|
|
||||||
case LOGGING_STATE_UNSTARTED:
|
|
||||||
mLoggingState = LOGGING_STATE_STOPPED;
|
|
||||||
isAbortSuccessful = true;
|
|
||||||
break;
|
|
||||||
case LOGGING_STATE_READY:
|
|
||||||
case LOGGING_STATE_RUNNING:
|
|
||||||
mExecutor.submit(new Callable<Object>() {
|
mExecutor.submit(new Callable<Object>() {
|
||||||
@Override
|
@Override
|
||||||
public Object call() throws Exception {
|
public Object call() throws Exception {
|
||||||
try {
|
try {
|
||||||
|
if (mHasWrittenData) {
|
||||||
mJsonWriter.endArray();
|
mJsonWriter.endArray();
|
||||||
mJsonWriter.close();
|
mJsonWriter.close();
|
||||||
|
mHasWrittenData = false;
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
isAbortSuccessful = mFile.delete();
|
mIsAbortSuccessful = mFile.delete();
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
removeAnyScheduledFlush();
|
removeAnyScheduledFlush();
|
||||||
mExecutor.shutdown();
|
mExecutor.shutdown();
|
||||||
mLoggingState = LOGGING_STATE_STOPPING;
|
|
||||||
break;
|
|
||||||
case LOGGING_STATE_STOPPING:
|
|
||||||
case LOGGING_STATE_STOPPED:
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isAbortSuccessful;
|
public boolean blockingAbort() throws InterruptedException {
|
||||||
public boolean isAbortSuccessful() {
|
abort();
|
||||||
return isAbortSuccessful;
|
mExecutor.awaitTermination(ABORT_TIMEOUT_IN_MS, TimeUnit.MILLISECONDS);
|
||||||
|
return mIsAbortSuccessful;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void awaitTermination(int delay, TimeUnit timeUnit) throws InterruptedException {
|
||||||
|
mExecutor.awaitTermination(delay, timeUnit);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* package */ synchronized void flush() {
|
/* package */ synchronized void flush() {
|
||||||
switch (mLoggingState) {
|
|
||||||
case LOGGING_STATE_UNSTARTED:
|
|
||||||
break;
|
|
||||||
case LOGGING_STATE_READY:
|
|
||||||
case LOGGING_STATE_RUNNING:
|
|
||||||
removeAnyScheduledFlush();
|
removeAnyScheduledFlush();
|
||||||
mExecutor.submit(mFlushCallable);
|
mExecutor.submit(mFlushCallable);
|
||||||
break;
|
|
||||||
case LOGGING_STATE_STOPPING:
|
|
||||||
case LOGGING_STATE_STOPPED:
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Callable<Object> mFlushCallable = new Callable<Object>() {
|
private final Callable<Object> mFlushCallable = new Callable<Object>() {
|
||||||
@Override
|
@Override
|
||||||
public Object call() throws Exception {
|
public Object call() throws Exception {
|
||||||
if (mLoggingState == LOGGING_STATE_RUNNING) {
|
|
||||||
mJsonWriter.flush();
|
mJsonWriter.flush();
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -224,56 +178,40 @@ public class ResearchLog {
|
||||||
mFlushFuture = mExecutor.schedule(mFlushCallable, FLUSH_DELAY_IN_MS, TimeUnit.MILLISECONDS);
|
mFlushFuture = mExecutor.schedule(mFlushCallable, FLUSH_DELAY_IN_MS, TimeUnit.MILLISECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void publishPublicEvents(final LogUnit logUnit) {
|
public synchronized void publish(final LogUnit logUnit, final boolean isIncludingPrivateData) {
|
||||||
switch (mLoggingState) {
|
try {
|
||||||
case LOGGING_STATE_UNSTARTED:
|
|
||||||
break;
|
|
||||||
case LOGGING_STATE_READY:
|
|
||||||
case LOGGING_STATE_RUNNING:
|
|
||||||
mExecutor.submit(new Callable<Object>() {
|
mExecutor.submit(new Callable<Object>() {
|
||||||
@Override
|
@Override
|
||||||
public Object call() throws Exception {
|
public Object call() throws Exception {
|
||||||
logUnit.publishPublicEventsTo(ResearchLog.this);
|
logUnit.publishTo(ResearchLog.this, isIncludingPrivateData);
|
||||||
scheduleFlush();
|
scheduleFlush();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
break;
|
} catch (RejectedExecutionException e) {
|
||||||
case LOGGING_STATE_STOPPING:
|
// TODO: Add code to record loss of data, and report.
|
||||||
case LOGGING_STATE_STOPPED:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized void publishAllEvents(final LogUnit logUnit) {
|
|
||||||
switch (mLoggingState) {
|
|
||||||
case LOGGING_STATE_UNSTARTED:
|
|
||||||
break;
|
|
||||||
case LOGGING_STATE_READY:
|
|
||||||
case LOGGING_STATE_RUNNING:
|
|
||||||
mExecutor.submit(new Callable<Object>() {
|
|
||||||
@Override
|
|
||||||
public Object call() throws Exception {
|
|
||||||
logUnit.publishAllEventsTo(ResearchLog.this);
|
|
||||||
scheduleFlush();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case LOGGING_STATE_STOPPING:
|
|
||||||
case LOGGING_STATE_STOPPED:
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final String CURRENT_TIME_KEY = "_ct";
|
private static final String CURRENT_TIME_KEY = "_ct";
|
||||||
private static final String UPTIME_KEY = "_ut";
|
private static final String UPTIME_KEY = "_ut";
|
||||||
private static final String EVENT_TYPE_KEY = "_ty";
|
private static final String EVENT_TYPE_KEY = "_ty";
|
||||||
|
|
||||||
void outputEvent(final String[] keys, final Object[] values) {
|
void outputEvent(final String[] keys, final Object[] values) {
|
||||||
// not thread safe.
|
// Not thread safe.
|
||||||
|
if (keys.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (DEBUG) {
|
||||||
|
if (keys.length != values.length + 1) {
|
||||||
|
Log.d(TAG, "Key and Value list sizes do not match. " + keys[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
if (mJsonWriter == NULL_JSON_WRITER) {
|
if (mJsonWriter == NULL_JSON_WRITER) {
|
||||||
mJsonWriter = new JsonWriter(new BufferedWriter(new FileWriter(mFile)));
|
mJsonWriter = new JsonWriter(new BufferedWriter(new FileWriter(mFile)));
|
||||||
mJsonWriter.setLenient(true);
|
|
||||||
mJsonWriter.beginArray();
|
mJsonWriter.beginArray();
|
||||||
|
mHasWrittenData = true;
|
||||||
}
|
}
|
||||||
mJsonWriter.beginObject();
|
mJsonWriter.beginObject();
|
||||||
mJsonWriter.name(CURRENT_TIME_KEY).value(System.currentTimeMillis());
|
mJsonWriter.name(CURRENT_TIME_KEY).value(System.currentTimeMillis());
|
||||||
|
@ -283,8 +221,8 @@ public class ResearchLog {
|
||||||
for (int i = 0; i < length; i++) {
|
for (int i = 0; i < length; i++) {
|
||||||
mJsonWriter.name(keys[i + 1]);
|
mJsonWriter.name(keys[i + 1]);
|
||||||
Object value = values[i];
|
Object value = values[i];
|
||||||
if (value instanceof String) {
|
if (value instanceof CharSequence) {
|
||||||
mJsonWriter.value((String) value);
|
mJsonWriter.value(value.toString());
|
||||||
} else if (value instanceof Number) {
|
} else if (value instanceof Number) {
|
||||||
mJsonWriter.value((Number) value);
|
mJsonWriter.value((Number) value);
|
||||||
} else if (value instanceof Boolean) {
|
} else if (value instanceof Boolean) {
|
||||||
|
@ -331,14 +269,11 @@ public class ResearchLog {
|
||||||
SuggestedWords words = (SuggestedWords) value;
|
SuggestedWords words = (SuggestedWords) value;
|
||||||
mJsonWriter.beginObject();
|
mJsonWriter.beginObject();
|
||||||
mJsonWriter.name("typedWordValid").value(words.mTypedWordValid);
|
mJsonWriter.name("typedWordValid").value(words.mTypedWordValid);
|
||||||
mJsonWriter.name("willAutoCorrect")
|
mJsonWriter.name("willAutoCorrect").value(words.mWillAutoCorrect);
|
||||||
.value(words.mWillAutoCorrect);
|
|
||||||
mJsonWriter.name("isPunctuationSuggestions")
|
mJsonWriter.name("isPunctuationSuggestions")
|
||||||
.value(words.mIsPunctuationSuggestions);
|
.value(words.mIsPunctuationSuggestions);
|
||||||
mJsonWriter.name("isObsoleteSuggestions")
|
mJsonWriter.name("isObsoleteSuggestions").value(words.mIsObsoleteSuggestions);
|
||||||
.value(words.mIsObsoleteSuggestions);
|
mJsonWriter.name("isPrediction").value(words.mIsPrediction);
|
||||||
mJsonWriter.name("isPrediction")
|
|
||||||
.value(words.mIsPrediction);
|
|
||||||
mJsonWriter.name("words");
|
mJsonWriter.name("words");
|
||||||
mJsonWriter.beginArray();
|
mJsonWriter.beginArray();
|
||||||
final int size = words.size();
|
final int size = words.size();
|
||||||
|
@ -363,8 +298,8 @@ public class ResearchLog {
|
||||||
try {
|
try {
|
||||||
mJsonWriter.close();
|
mJsonWriter.close();
|
||||||
} catch (IllegalStateException e1) {
|
} catch (IllegalStateException e1) {
|
||||||
// assume that this is just the json not being terminated properly.
|
// Assume that this is just the json not being terminated properly.
|
||||||
// ignore
|
// Ignore
|
||||||
} catch (IOException e1) {
|
} catch (IOException e1) {
|
||||||
e1.printStackTrace();
|
e1.printStackTrace();
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
@ -27,7 +27,6 @@ import android.os.BatteryManager;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import com.android.inputmethod.latin.R;
|
import com.android.inputmethod.latin.R;
|
||||||
import com.android.inputmethod.latin.R.string;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
@ -48,6 +47,7 @@ public final class ResearchLogUploader {
|
||||||
private static final String TAG = ResearchLogUploader.class.getSimpleName();
|
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 UPLOAD_INTERVAL_IN_MS = 1000 * 60 * 15; // every 15 min
|
||||||
private static final int BUF_SIZE = 1024 * 8;
|
private static final int BUF_SIZE = 1024 * 8;
|
||||||
|
protected static final int TIMEOUT_IN_MS = 1000 * 4;
|
||||||
|
|
||||||
private final boolean mCanUpload;
|
private final boolean mCanUpload;
|
||||||
private final Context mContext;
|
private final Context mContext;
|
||||||
|
@ -55,8 +55,6 @@ public final class ResearchLogUploader {
|
||||||
private final URL mUrl;
|
private final URL mUrl;
|
||||||
private final ScheduledExecutorService mExecutor;
|
private final ScheduledExecutorService mExecutor;
|
||||||
|
|
||||||
private Runnable doUploadRunnable = new UploadRunnable(null, false);
|
|
||||||
|
|
||||||
public ResearchLogUploader(final Context context, final File filesDir) {
|
public ResearchLogUploader(final Context context, final File filesDir) {
|
||||||
mContext = context;
|
mContext = context;
|
||||||
mFilesDir = filesDir;
|
mFilesDir = filesDir;
|
||||||
|
@ -93,11 +91,15 @@ public final class ResearchLogUploader {
|
||||||
|
|
||||||
public void start() {
|
public void start() {
|
||||||
if (mCanUpload) {
|
if (mCanUpload) {
|
||||||
Log.d(TAG, "scheduling regular uploading");
|
mExecutor.scheduleWithFixedDelay(new UploadRunnable(null /* logToWaitFor */,
|
||||||
mExecutor.scheduleWithFixedDelay(doUploadRunnable, UPLOAD_INTERVAL_IN_MS,
|
null /* callback */, false /* forceUpload */),
|
||||||
UPLOAD_INTERVAL_IN_MS, TimeUnit.MILLISECONDS);
|
UPLOAD_INTERVAL_IN_MS, UPLOAD_INTERVAL_IN_MS, TimeUnit.MILLISECONDS);
|
||||||
} else {
|
}
|
||||||
Log.d(TAG, "no permission to upload");
|
}
|
||||||
|
|
||||||
|
public void uploadAfterCompletion(final ResearchLog researchLog, final Callback callback) {
|
||||||
|
if (mCanUpload) {
|
||||||
|
mExecutor.submit(new UploadRunnable(researchLog, callback, true /* forceUpload */));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,7 +108,8 @@ public final class ResearchLogUploader {
|
||||||
// another upload happening right now, as it may have missed the latest changes.
|
// another upload happening right now, as it may have missed the latest changes.
|
||||||
// TODO: Reschedule regular upload tests starting from now.
|
// TODO: Reschedule regular upload tests starting from now.
|
||||||
if (mCanUpload) {
|
if (mCanUpload) {
|
||||||
mExecutor.submit(new UploadRunnable(callback, true));
|
mExecutor.submit(new UploadRunnable(null /* logToWaitFor */, callback,
|
||||||
|
true /* forceUpload */));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,19 +133,33 @@ public final class ResearchLogUploader {
|
||||||
}
|
}
|
||||||
|
|
||||||
class UploadRunnable implements Runnable {
|
class UploadRunnable implements Runnable {
|
||||||
|
private final ResearchLog mLogToWaitFor;
|
||||||
private final Callback mCallback;
|
private final Callback mCallback;
|
||||||
private final boolean mForceUpload;
|
private final boolean mForceUpload;
|
||||||
|
|
||||||
public UploadRunnable(final Callback callback, final boolean forceUpload) {
|
public UploadRunnable(final ResearchLog logToWaitFor, final Callback callback,
|
||||||
|
final boolean forceUpload) {
|
||||||
|
mLogToWaitFor = logToWaitFor;
|
||||||
mCallback = callback;
|
mCallback = callback;
|
||||||
mForceUpload = forceUpload;
|
mForceUpload = forceUpload;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
if (mLogToWaitFor != null) {
|
||||||
|
waitFor(mLogToWaitFor);
|
||||||
|
}
|
||||||
doUpload();
|
doUpload();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void waitFor(final ResearchLog researchLog) {
|
||||||
|
try {
|
||||||
|
researchLog.awaitTermination(TIMEOUT_IN_MS, TimeUnit.MILLISECONDS);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void doUpload() {
|
private void doUpload() {
|
||||||
if (!mForceUpload && (!isExternallyPowered() || !hasWifiConnection())) {
|
if (!mForceUpload && (!isExternallyPowered() || !hasWifiConnection())) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -34,6 +34,7 @@ import android.graphics.Paint.Style;
|
||||||
import android.inputmethodservice.InputMethodService;
|
import android.inputmethodservice.InputMethodService;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
|
import android.os.SystemClock;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.text.format.DateUtils;
|
import android.text.format.DateUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
@ -66,11 +67,8 @@ import com.android.inputmethod.latin.SuggestedWords;
|
||||||
import com.android.inputmethod.latin.define.ProductionFlag;
|
import com.android.inputmethod.latin.define.ProductionFlag;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@ -96,24 +94,21 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
new SimpleDateFormat("yyyyMMddHHmmssS", Locale.US);
|
new SimpleDateFormat("yyyyMMddHHmmssS", Locale.US);
|
||||||
private static final boolean IS_SHOWING_INDICATOR = true;
|
private static final boolean IS_SHOWING_INDICATOR = true;
|
||||||
private static final boolean IS_SHOWING_INDICATOR_CLEARLY = false;
|
private static final boolean IS_SHOWING_INDICATOR_CLEARLY = false;
|
||||||
|
public static final int FEEDBACK_WORD_BUFFER_SIZE = 5;
|
||||||
|
|
||||||
// constants related to specific log points
|
// constants related to specific log points
|
||||||
private static final String WHITESPACE_SEPARATORS = " \t\n\r";
|
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 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 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();
|
private static final ResearchLogger sInstance = new ResearchLogger();
|
||||||
// to write to a different filename, e.g., for testing, set mFile before calling start()
|
// to write to a different filename, e.g., for testing, set mFile before calling start()
|
||||||
/* package */ File mFilesDir;
|
/* package */ File mFilesDir;
|
||||||
/* package */ String mUUIDString;
|
/* package */ String mUUIDString;
|
||||||
/* package */ ResearchLog mMainResearchLog;
|
/* package */ ResearchLog mMainResearchLog;
|
||||||
// The mIntentionalResearchLog records all events for the session, private or not (excepting
|
/* package */ ResearchLog mFeedbackLog;
|
||||||
// passwords). It is written to permanent storage only if the user explicitly commands
|
/* package */ MainLogBuffer mMainLogBuffer;
|
||||||
// the system to do so.
|
/* package */ LogBuffer mFeedbackLogBuffer;
|
||||||
/* package */ ResearchLog mIntentionalResearchLog;
|
|
||||||
// LogUnits are queued here and released only when the user requests the intentional log.
|
|
||||||
private List<LogUnit> mIntentionalResearchLogQueue = new ArrayList<LogUnit>();
|
|
||||||
|
|
||||||
private boolean mIsPasswordView = false;
|
private boolean mIsPasswordView = false;
|
||||||
private boolean mIsLoggingSuspended = false;
|
private boolean mIsLoggingSuspended = false;
|
||||||
|
@ -137,10 +132,13 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
private Dictionary mDictionary;
|
private Dictionary mDictionary;
|
||||||
private KeyboardSwitcher mKeyboardSwitcher;
|
private KeyboardSwitcher mKeyboardSwitcher;
|
||||||
private InputMethodService mInputMethodService;
|
private InputMethodService mInputMethodService;
|
||||||
|
private final Statistics mStatistics;
|
||||||
private ResearchLogUploader mResearchLogUploader;
|
private ResearchLogUploader mResearchLogUploader;
|
||||||
|
|
||||||
|
private LogUnit mCurrentLogUnit = new LogUnit();
|
||||||
|
|
||||||
private ResearchLogger() {
|
private ResearchLogger() {
|
||||||
|
mStatistics = Statistics.getInstance();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ResearchLogger getInstance() {
|
public static ResearchLogger getInstance() {
|
||||||
|
@ -261,50 +259,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
e.apply();
|
e.apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
private File createLogFile(File filesDir) {
|
|
||||||
final StringBuilder sb = new StringBuilder();
|
|
||||||
sb.append(FILENAME_PREFIX).append('-');
|
|
||||||
sb.append(mUUIDString).append('-');
|
|
||||||
sb.append(TIMESTAMP_DATEFORMAT.format(new Date()));
|
|
||||||
sb.append(FILENAME_SUFFIX);
|
|
||||||
return new File(filesDir, sb.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void start() {
|
|
||||||
maybeShowSplashScreen();
|
|
||||||
updateSuspendedState();
|
|
||||||
requestIndicatorRedraw();
|
|
||||||
if (!isAllowedToLog()) {
|
|
||||||
// Log.w(TAG, "not in usability mode; not logging");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (mFilesDir == null || !mFilesDir.exists()) {
|
|
||||||
Log.w(TAG, "IME storage directory does not exist. Cannot start logging.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
if (mMainResearchLog == null || !mMainResearchLog.isAlive()) {
|
|
||||||
mMainResearchLog = new ResearchLog(createLogFile(mFilesDir));
|
|
||||||
}
|
|
||||||
mMainResearchLog.start();
|
|
||||||
if (mIntentionalResearchLog == null || !mIntentionalResearchLog.isAlive()) {
|
|
||||||
mIntentionalResearchLog = new ResearchLog(createLogFile(mFilesDir));
|
|
||||||
}
|
|
||||||
mIntentionalResearchLog.start();
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.w(TAG, "Could not start ResearchLogger.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* package */ void stop() {
|
|
||||||
if (mMainResearchLog != null) {
|
|
||||||
mMainResearchLog.stop();
|
|
||||||
}
|
|
||||||
if (mIntentionalResearchLog != null) {
|
|
||||||
mIntentionalResearchLog.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setLoggingAllowed(boolean enableLogging) {
|
private void setLoggingAllowed(boolean enableLogging) {
|
||||||
if (mPrefs == null) {
|
if (mPrefs == null) {
|
||||||
return;
|
return;
|
||||||
|
@ -315,40 +269,104 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
sIsLogging = enableLogging;
|
sIsLogging = enableLogging;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean abort() {
|
private File createLogFile(File filesDir) {
|
||||||
boolean didAbortMainLog = false;
|
final StringBuilder sb = new StringBuilder();
|
||||||
if (mMainResearchLog != null) {
|
sb.append(FILENAME_PREFIX).append('-');
|
||||||
mMainResearchLog.abort();
|
sb.append(mUUIDString).append('-');
|
||||||
try {
|
sb.append(TIMESTAMP_DATEFORMAT.format(new Date()));
|
||||||
mMainResearchLog.waitUntilStopped(ABORT_TIMEOUT_IN_MS);
|
sb.append(FILENAME_SUFFIX);
|
||||||
} catch (InterruptedException e) {
|
return new File(filesDir, sb.toString());
|
||||||
// interrupted early. carry on.
|
|
||||||
}
|
|
||||||
if (mMainResearchLog.isAbortSuccessful()) {
|
|
||||||
didAbortMainLog = true;
|
|
||||||
}
|
|
||||||
mMainResearchLog = null;
|
|
||||||
}
|
|
||||||
boolean didAbortIntentionalLog = false;
|
|
||||||
if (mIntentionalResearchLog != null) {
|
|
||||||
mIntentionalResearchLog.abort();
|
|
||||||
try {
|
|
||||||
mIntentionalResearchLog.waitUntilStopped(ABORT_TIMEOUT_IN_MS);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
// interrupted early. carry on.
|
|
||||||
}
|
|
||||||
if (mIntentionalResearchLog.isAbortSuccessful()) {
|
|
||||||
didAbortIntentionalLog = true;
|
|
||||||
}
|
|
||||||
mIntentionalResearchLog = null;
|
|
||||||
}
|
|
||||||
return didAbortMainLog && didAbortIntentionalLog;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* package */ void flush() {
|
private void checkForEmptyEditor() {
|
||||||
if (mMainResearchLog != null) {
|
if (mInputMethodService == null) {
|
||||||
mMainResearchLog.flush();
|
return;
|
||||||
}
|
}
|
||||||
|
final InputConnection ic = mInputMethodService.getCurrentInputConnection();
|
||||||
|
if (ic == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final CharSequence textBefore = ic.getTextBeforeCursor(1, 0);
|
||||||
|
if (!TextUtils.isEmpty(textBefore)) {
|
||||||
|
mStatistics.setIsEmptyUponStarting(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final CharSequence textAfter = ic.getTextAfterCursor(1, 0);
|
||||||
|
if (!TextUtils.isEmpty(textAfter)) {
|
||||||
|
mStatistics.setIsEmptyUponStarting(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (textBefore != null && textAfter != null) {
|
||||||
|
mStatistics.setIsEmptyUponStarting(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void start() {
|
||||||
|
maybeShowSplashScreen();
|
||||||
|
updateSuspendedState();
|
||||||
|
requestIndicatorRedraw();
|
||||||
|
mStatistics.reset();
|
||||||
|
checkForEmptyEditor();
|
||||||
|
if (!isAllowedToLog()) {
|
||||||
|
// Log.w(TAG, "not in usability mode; not logging");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mFilesDir == null || !mFilesDir.exists()) {
|
||||||
|
Log.w(TAG, "IME storage directory does not exist. Cannot start logging.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mMainLogBuffer == null) {
|
||||||
|
mMainResearchLog = new ResearchLog(createLogFile(mFilesDir));
|
||||||
|
mMainLogBuffer = new MainLogBuffer(mMainResearchLog);
|
||||||
|
mMainLogBuffer.setSuggest(mSuggest);
|
||||||
|
}
|
||||||
|
if (mFeedbackLogBuffer == null) {
|
||||||
|
mFeedbackLog = new ResearchLog(createLogFile(mFilesDir));
|
||||||
|
// LogBuffer is one more than FEEDBACK_WORD_BUFFER_SIZE, because it must also hold
|
||||||
|
// the feedback LogUnit itself.
|
||||||
|
mFeedbackLogBuffer = new LogBuffer(FEEDBACK_WORD_BUFFER_SIZE + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* package */ void stop() {
|
||||||
|
logStatistics();
|
||||||
|
commitCurrentLogUnit();
|
||||||
|
|
||||||
|
if (mMainLogBuffer != null) {
|
||||||
|
publishLogBuffer(mMainLogBuffer, mMainResearchLog, false /* isIncludingPrivateData */);
|
||||||
|
mMainResearchLog.close();
|
||||||
|
mMainLogBuffer = null;
|
||||||
|
}
|
||||||
|
if (mFeedbackLogBuffer != null) {
|
||||||
|
mFeedbackLog.close();
|
||||||
|
mFeedbackLogBuffer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean abort() {
|
||||||
|
boolean didAbortMainLog = false;
|
||||||
|
if (mMainLogBuffer != null) {
|
||||||
|
mMainLogBuffer.clear();
|
||||||
|
try {
|
||||||
|
didAbortMainLog = mMainResearchLog.blockingAbort();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// Don't know whether this succeeded or not. We assume not; this is reported
|
||||||
|
// to the caller.
|
||||||
|
}
|
||||||
|
mMainLogBuffer = null;
|
||||||
|
}
|
||||||
|
boolean didAbortFeedbackLog = false;
|
||||||
|
if (mFeedbackLogBuffer != null) {
|
||||||
|
mFeedbackLogBuffer.clear();
|
||||||
|
try {
|
||||||
|
didAbortFeedbackLog = mFeedbackLog.blockingAbort();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// Don't know whether this succeeded or not. We assume not; this is reported
|
||||||
|
// to the caller.
|
||||||
|
}
|
||||||
|
mFeedbackLogBuffer = null;
|
||||||
|
}
|
||||||
|
return didAbortMainLog && didAbortFeedbackLog;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void restart() {
|
private void restart() {
|
||||||
|
@ -452,79 +470,39 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
latinIME.launchKeyboardedDialogActivity(FeedbackActivity.class);
|
latinIME.launchKeyboardedDialogActivity(FeedbackActivity.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ResearchLog mFeedbackLog;
|
private static final String[] EVENTKEYS_FEEDBACK = {
|
||||||
private List<LogUnit> mFeedbackQueue;
|
"UserTimestamp", "contents"
|
||||||
private ResearchLog mSavedMainResearchLog;
|
};
|
||||||
private ResearchLog mSavedIntentionalResearchLog;
|
|
||||||
private List<LogUnit> mSavedIntentionalResearchLogQueue;
|
|
||||||
|
|
||||||
private void saveLogsForFeedback() {
|
|
||||||
mFeedbackLog = mIntentionalResearchLog;
|
|
||||||
if (mIntentionalResearchLogQueue != null) {
|
|
||||||
mFeedbackQueue = new ArrayList<LogUnit>(mIntentionalResearchLogQueue);
|
|
||||||
} else {
|
|
||||||
mFeedbackQueue = null;
|
|
||||||
}
|
|
||||||
mSavedMainResearchLog = mMainResearchLog;
|
|
||||||
mSavedIntentionalResearchLog = mIntentionalResearchLog;
|
|
||||||
mSavedIntentionalResearchLogQueue = mIntentionalResearchLogQueue;
|
|
||||||
|
|
||||||
mMainResearchLog = null;
|
|
||||||
mIntentionalResearchLog = null;
|
|
||||||
mIntentionalResearchLogQueue = new ArrayList<LogUnit>();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final int LOG_DRAIN_TIMEOUT_IN_MS = 1000 * 5;
|
|
||||||
public void sendFeedback(final String feedbackContents, final boolean includeHistory) {
|
public void sendFeedback(final String feedbackContents, final boolean includeHistory) {
|
||||||
if (includeHistory && mFeedbackLog != null) {
|
if (mFeedbackLogBuffer == null) {
|
||||||
try {
|
return;
|
||||||
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() {
|
public void onLeavingSendFeedbackDialog() {
|
||||||
mInFeedbackDialog = false;
|
mInFeedbackDialog = false;
|
||||||
mMainResearchLog = mSavedMainResearchLog;
|
|
||||||
mIntentionalResearchLog = mSavedIntentionalResearchLog;
|
|
||||||
mIntentionalResearchLogQueue = mSavedIntentionalResearchLogQueue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void initSuggest(Suggest suggest) {
|
public void initSuggest(Suggest suggest) {
|
||||||
mSuggest = suggest;
|
mSuggest = suggest;
|
||||||
|
if (mMainLogBuffer != null) {
|
||||||
|
mMainLogBuffer.setSuggest(mSuggest);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setIsPasswordView(boolean isPasswordView) {
|
private void setIsPasswordView(boolean isPasswordView) {
|
||||||
|
@ -532,7 +510,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isAllowedToLog() {
|
private boolean isAllowedToLog() {
|
||||||
return !mIsPasswordView && !mIsLoggingSuspended && sIsLogging;
|
return !mIsPasswordView && !mIsLoggingSuspended && sIsLogging && !mInFeedbackDialog;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void requestIndicatorRedraw() {
|
public void requestIndicatorRedraw() {
|
||||||
|
@ -579,13 +557,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final String CURRENT_TIME_KEY = "_ct";
|
|
||||||
private static final String UPTIME_KEY = "_ut";
|
|
||||||
private static final String EVENT_TYPE_KEY = "_ty";
|
|
||||||
private static final Object[] EVENTKEYS_NULLVALUES = {};
|
private static final Object[] EVENTKEYS_NULLVALUES = {};
|
||||||
|
|
||||||
private LogUnit mCurrentLogUnit = new LogUnit();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Buffer a research log event, flagging it as privacy-sensitive.
|
* Buffer a research log event, flagging it as privacy-sensitive.
|
||||||
*
|
*
|
||||||
|
@ -601,10 +574,14 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
final Object[] values) {
|
final Object[] values) {
|
||||||
assert values.length + 1 == keys.length;
|
assert values.length + 1 == keys.length;
|
||||||
if (isAllowedToLog()) {
|
if (isAllowedToLog()) {
|
||||||
mCurrentLogUnit.addLogAtom(keys, values, true);
|
mCurrentLogUnit.addLogStatement(keys, values, true /* isPotentiallyPrivate */);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setCurrentLogUnitContainsDigitFlag() {
|
||||||
|
mCurrentLogUnit.setContainsDigit();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Buffer a research log event, flaggint it as not privacy-sensitive.
|
* Buffer a research log event, flaggint it as not privacy-sensitive.
|
||||||
*
|
*
|
||||||
|
@ -620,139 +597,54 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
private synchronized void enqueueEvent(final String[] keys, final Object[] values) {
|
private synchronized void enqueueEvent(final String[] keys, final Object[] values) {
|
||||||
assert values.length + 1 == keys.length;
|
assert values.length + 1 == keys.length;
|
||||||
if (isAllowedToLog()) {
|
if (isAllowedToLog()) {
|
||||||
mCurrentLogUnit.addLogAtom(keys, values, false);
|
mCurrentLogUnit.addLogStatement(keys, values, false /* isPotentiallyPrivate */);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Used to track how often words are logged. Too-frequent logging can leak
|
/* package for test */ void commitCurrentLogUnit() {
|
||||||
// semantics, disclosing private data.
|
if (!mCurrentLogUnit.isEmpty()) {
|
||||||
/* package for test */ static class LoggingFrequencyState {
|
if (mMainLogBuffer != null) {
|
||||||
private static final int DEFAULT_WORD_LOG_FREQUENCY = 10;
|
mMainLogBuffer.shiftIn(mCurrentLogUnit);
|
||||||
private int mWordsRemainingToSkip;
|
if (mMainLogBuffer.isSafeToLog() && mMainResearchLog != null) {
|
||||||
private final int mFrequency;
|
publishLogBuffer(mMainLogBuffer, mMainResearchLog,
|
||||||
|
true /* isIncludingPrivateData */);
|
||||||
/**
|
mMainLogBuffer.resetWordCounter();
|
||||||
* 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--;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (mFeedbackLogBuffer != null) {
|
||||||
public boolean isSafeToLog() {
|
mFeedbackLogBuffer.shiftIn(mCurrentLogUnit);
|
||||||
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();
|
mCurrentLogUnit = new LogUnit();
|
||||||
}
|
Log.d(TAG, "commitCurrentLogUnit");
|
||||||
|
|
||||||
private void publishLogUnit(LogUnit logUnit, boolean isPrivacySensitive) {
|
|
||||||
if (!isAllowedToLog()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (mMainResearchLog == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isPrivacySensitive) {
|
|
||||||
mMainResearchLog.publishPublicEvents(logUnit);
|
|
||||||
} else {
|
|
||||||
mMainResearchLog.publishAllEvents(logUnit);
|
|
||||||
}
|
|
||||||
mIntentionalResearchLogQueue.add(logUnit);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* package */ void publishCurrentLogUnit(ResearchLog researchLog, boolean isPrivacySensitive) {
|
|
||||||
publishLogUnit(mCurrentLogUnit, isPrivacySensitive);
|
|
||||||
}
|
|
||||||
|
|
||||||
static class LogUnit {
|
|
||||||
private final List<String[]> mKeysList = new ArrayList<String[]>();
|
|
||||||
private final List<Object[]> mValuesList = new ArrayList<Object[]>();
|
|
||||||
private final List<Boolean> mIsPotentiallyPrivate = new ArrayList<Boolean>();
|
|
||||||
|
|
||||||
private void addLogAtom(final String[] keys, final Object[] values,
|
|
||||||
final Boolean isPotentiallyPrivate) {
|
|
||||||
mKeysList.add(keys);
|
|
||||||
mValuesList.add(values);
|
|
||||||
mIsPotentiallyPrivate.add(isPotentiallyPrivate);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void publishPublicEventsTo(ResearchLog researchLog) {
|
|
||||||
final int size = mKeysList.size();
|
|
||||||
for (int i = 0; i < size; i++) {
|
|
||||||
if (!mIsPotentiallyPrivate.get(i)) {
|
|
||||||
researchLog.outputEvent(mKeysList.get(i), mValuesList.get(i));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void publishAllEventsTo(ResearchLog researchLog) {
|
/* package for test */ void publishLogBuffer(final LogBuffer logBuffer,
|
||||||
final int size = mKeysList.size();
|
final ResearchLog researchLog, final boolean isIncludingPrivateData) {
|
||||||
for (int i = 0; i < size; i++) {
|
LogUnit logUnit;
|
||||||
researchLog.outputEvent(mKeysList.get(i), mValuesList.get(i));
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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) {
|
private static int scrubDigitFromCodePoint(int codePoint) {
|
||||||
|
@ -805,12 +697,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
return WORD_REPLACEMENT_STRING;
|
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 = {
|
private static final String[] EVENTKEYS_LATINIME_ONSTARTINPUTVIEWINTERNAL = {
|
||||||
"LatinIMEOnStartInputViewInternal", "uuid", "packageName", "inputType", "imeOptions",
|
"LatinIMEOnStartInputViewInternal", "uuid", "packageName", "inputType", "imeOptions",
|
||||||
"fieldId", "display", "model", "prefs", "versionCode", "versionName", "outputFormatVersion"
|
"fieldId", "display", "model", "prefs", "versionCode", "versionName", "outputFormatVersion"
|
||||||
|
@ -818,9 +704,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
public static void latinIME_onStartInputViewInternal(final EditorInfo editorInfo,
|
public static void latinIME_onStartInputViewInternal(final EditorInfo editorInfo,
|
||||||
final SharedPreferences prefs) {
|
final SharedPreferences prefs) {
|
||||||
final ResearchLogger researchLogger = getInstance();
|
final ResearchLogger researchLogger = getInstance();
|
||||||
if (researchLogger.mInFeedbackDialog) {
|
|
||||||
researchLogger.saveLogsForFeedback();
|
|
||||||
}
|
|
||||||
researchLogger.start();
|
researchLogger.start();
|
||||||
if (editorInfo != null) {
|
if (editorInfo != null) {
|
||||||
final Context context = researchLogger.mInputMethodService;
|
final Context context = researchLogger.mInputMethodService;
|
||||||
|
@ -852,16 +735,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
"UserFeedback", "FeedbackContents"
|
"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
|
// Regular logging methods
|
||||||
|
|
||||||
private static final String[] EVENTKEYS_MAINKEYBOARDVIEW_PROCESSMOTIONEVENT = {
|
private static final String[] EVENTKEYS_MAINKEYBOARDVIEW_PROCESSMOTIONEVENT = {
|
||||||
|
@ -896,10 +769,16 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
"LatinIMEOnCodeInput", "code", "x", "y"
|
"LatinIMEOnCodeInput", "code", "x", "y"
|
||||||
};
|
};
|
||||||
public static void latinIME_onCodeInput(final int code, final int x, final int 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 = {
|
final Object[] values = {
|
||||||
Keyboard.printableCode(scrubDigitFromCodePoint(code)), x, y
|
Keyboard.printableCode(scrubDigitFromCodePoint(code)), x, y
|
||||||
};
|
};
|
||||||
getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_ONCODEINPUT, values);
|
researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_ONCODEINPUT, values);
|
||||||
|
if (Character.isDigit(code)) {
|
||||||
|
researchLogger.setCurrentLogUnitContainsDigitFlag();
|
||||||
|
}
|
||||||
|
researchLogger.mStatistics.recordChar(code, time);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final String[] EVENTKEYS_LATINIME_ONDISPLAYCOMPLETIONS = {
|
private static final String[] EVENTKEYS_LATINIME_ONDISPLAYCOMPLETIONS = {
|
||||||
|
@ -964,9 +843,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
}
|
}
|
||||||
final ResearchLogger researchLogger = getInstance();
|
final ResearchLogger researchLogger = getInstance();
|
||||||
researchLogger.enqueueEvent(EVENTKEYS_LATINIME_ONWINDOWHIDDEN, values);
|
researchLogger.enqueueEvent(EVENTKEYS_LATINIME_ONWINDOWHIDDEN, values);
|
||||||
// Play it safe. Remove privacy-sensitive events.
|
researchLogger.commitCurrentLogUnit();
|
||||||
researchLogger.publishLogUnit(researchLogger.mCurrentLogUnit, true);
|
|
||||||
researchLogger.mCurrentLogUnit = new LogUnit();
|
|
||||||
getInstance().stop();
|
getInstance().stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1031,7 +908,11 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
final Object[] values = {
|
final Object[] values = {
|
||||||
Keyboard.printableCode(scrubDigitFromCodePoint(code))
|
Keyboard.printableCode(scrubDigitFromCodePoint(code))
|
||||||
};
|
};
|
||||||
getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_SENDKEYCODEPOINT, values);
|
final ResearchLogger researchLogger = getInstance();
|
||||||
|
researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_SENDKEYCODEPOINT, values);
|
||||||
|
if (Character.isDigit(code)) {
|
||||||
|
researchLogger.setCurrentLogUnitContainsDigitFlag();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final String[] EVENTKEYS_LATINIME_SWAPSWAPPERANDSPACE = {
|
private static final String[] EVENTKEYS_LATINIME_SWAPSWAPPERANDSPACE = {
|
||||||
|
@ -1169,10 +1050,21 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
EVENTKEYS_RICHINPUTCONNECTION_COMMITCOMPLETION, values);
|
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 = {
|
private static final String[] EVENTKEYS_RICHINPUTCONNECTION_COMMITCORRECTION = {
|
||||||
"RichInputConnectionCommitCorrection", "CorrectionInfo"
|
"RichInputConnectionCommitCorrection", "typedWord", "autoCorrection"
|
||||||
};
|
};
|
||||||
|
*/
|
||||||
public static void richInputConnection_commitCorrection(CorrectionInfo correctionInfo) {
|
public static void richInputConnection_commitCorrection(CorrectionInfo correctionInfo) {
|
||||||
|
/*
|
||||||
final String typedWord = correctionInfo.getOldText().toString();
|
final String typedWord = correctionInfo.getOldText().toString();
|
||||||
final String autoCorrection = correctionInfo.getNewText().toString();
|
final String autoCorrection = correctionInfo.getNewText().toString();
|
||||||
final Object[] values = {
|
final Object[] values = {
|
||||||
|
@ -1181,6 +1073,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
final ResearchLogger researchLogger = getInstance();
|
final ResearchLogger researchLogger = getInstance();
|
||||||
researchLogger.enqueuePotentiallyPrivateEvent(
|
researchLogger.enqueuePotentiallyPrivateEvent(
|
||||||
EVENTKEYS_RICHINPUTCONNECTION_COMMITCORRECTION, values);
|
EVENTKEYS_RICHINPUTCONNECTION_COMMITCORRECTION, values);
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final String[] EVENTKEYS_RICHINPUTCONNECTION_COMMITTEXT = {
|
private static final String[] EVENTKEYS_RICHINPUTCONNECTION_COMMITTEXT = {
|
||||||
|
@ -1206,7 +1099,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
final Object[] values = {
|
final Object[] values = {
|
||||||
beforeLength, afterLength
|
beforeLength, afterLength
|
||||||
};
|
};
|
||||||
getInstance().enqueueEvent(EVENTKEYS_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT, values);
|
getInstance().enqueuePotentiallyPrivateEvent(
|
||||||
|
EVENTKEYS_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT, values);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final String[] EVENTKEYS_RICHINPUTCONNECTION_FINISHCOMPOSINGTEXT = {
|
private static final String[] EVENTKEYS_RICHINPUTCONNECTION_FINISHCOMPOSINGTEXT = {
|
||||||
|
@ -1236,7 +1130,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
keyEvent.getAction(),
|
keyEvent.getAction(),
|
||||||
keyEvent.getKeyCode()
|
keyEvent.getKeyCode()
|
||||||
};
|
};
|
||||||
getInstance().enqueueEvent(EVENTKEYS_RICHINPUTCONNECTION_SENDKEYEVENT, values);
|
getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_RICHINPUTCONNECTION_SENDKEYEVENT,
|
||||||
|
values);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final String[] EVENTKEYS_RICHINPUTCONNECTION_SETCOMPOSINGTEXT = {
|
private static final String[] EVENTKEYS_RICHINPUTCONNECTION_SETCOMPOSINGTEXT = {
|
||||||
|
@ -1244,10 +1139,14 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
};
|
};
|
||||||
public static void richInputConnection_setComposingText(final CharSequence text,
|
public static void richInputConnection_setComposingText(final CharSequence text,
|
||||||
final int newCursorPosition) {
|
final int newCursorPosition) {
|
||||||
|
if (text == null) {
|
||||||
|
throw new RuntimeException("setComposingText is null");
|
||||||
|
}
|
||||||
final Object[] values = {
|
final Object[] values = {
|
||||||
text, newCursorPosition
|
text, newCursorPosition
|
||||||
};
|
};
|
||||||
getInstance().enqueueEvent(EVENTKEYS_RICHINPUTCONNECTION_SETCOMPOSINGTEXT, values);
|
getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_RICHINPUTCONNECTION_SETCOMPOSINGTEXT,
|
||||||
|
values);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final String[] EVENTKEYS_RICHINPUTCONNECTION_SETSELECTION = {
|
private static final String[] EVENTKEYS_RICHINPUTCONNECTION_SETSELECTION = {
|
||||||
|
@ -1257,7 +1156,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
final Object[] values = {
|
final Object[] values = {
|
||||||
from, to
|
from, to
|
||||||
};
|
};
|
||||||
getInstance().enqueueEvent(EVENTKEYS_RICHINPUTCONNECTION_SETSELECTION, values);
|
getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_RICHINPUTCONNECTION_SETSELECTION,
|
||||||
|
values);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final String[] EVENTKEYS_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT = {
|
private static final String[] EVENTKEYS_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT = {
|
||||||
|
@ -1292,4 +1192,24 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
public void userTimestamp() {
|
public void userTimestamp() {
|
||||||
getInstance().enqueueEvent(EVENTKEYS_USER_TIMESTAMP, EVENTKEYS_NULLVALUES);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,146 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2012 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy of
|
||||||
|
* the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.inputmethod.research;
|
||||||
|
|
||||||
|
import com.android.inputmethod.keyboard.Keyboard;
|
||||||
|
|
||||||
|
public class Statistics {
|
||||||
|
// Number of characters entered during a typing session
|
||||||
|
int mCharCount;
|
||||||
|
// Number of letter characters entered during a typing session
|
||||||
|
int mLetterCount;
|
||||||
|
// Number of number characters entered
|
||||||
|
int mNumberCount;
|
||||||
|
// Number of space characters entered
|
||||||
|
int mSpaceCount;
|
||||||
|
// Number of delete operations entered (taps on the backspace key)
|
||||||
|
int mDeleteKeyCount;
|
||||||
|
// Number of words entered during a session.
|
||||||
|
int mWordCount;
|
||||||
|
// Whether the text field was empty upon editing
|
||||||
|
boolean mIsEmptyUponStarting;
|
||||||
|
boolean mIsEmptinessStateKnown;
|
||||||
|
|
||||||
|
// Timers to count average time to enter a key, first press a delete key,
|
||||||
|
// between delete keys, and then to return typing after a delete key.
|
||||||
|
final AverageTimeCounter mKeyCounter = new AverageTimeCounter();
|
||||||
|
final AverageTimeCounter mBeforeDeleteKeyCounter = new AverageTimeCounter();
|
||||||
|
final AverageTimeCounter mDuringRepeatedDeleteKeysCounter = new AverageTimeCounter();
|
||||||
|
final AverageTimeCounter mAfterDeleteKeyCounter = new AverageTimeCounter();
|
||||||
|
|
||||||
|
static class AverageTimeCounter {
|
||||||
|
int mCount;
|
||||||
|
int mTotalTime;
|
||||||
|
|
||||||
|
public void reset() {
|
||||||
|
mCount = 0;
|
||||||
|
mTotalTime = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void add(long deltaTime) {
|
||||||
|
mCount++;
|
||||||
|
mTotalTime += deltaTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getAverageTime() {
|
||||||
|
if (mCount == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return mTotalTime / mCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// To account for the interruptions when the user's attention is directed elsewhere, times
|
||||||
|
// longer than MIN_TYPING_INTERMISSION are not counted when estimating this statistic.
|
||||||
|
public static final int MIN_TYPING_INTERMISSION = 2 * 1000; // in milliseconds
|
||||||
|
public static final int MIN_DELETION_INTERMISSION = 10 * 1000; // in milliseconds
|
||||||
|
|
||||||
|
// The last time that a tap was performed
|
||||||
|
private long mLastTapTime;
|
||||||
|
// The type of the last keypress (delete key or not)
|
||||||
|
boolean mIsLastKeyDeleteKey;
|
||||||
|
|
||||||
|
private static final Statistics sInstance = new Statistics();
|
||||||
|
|
||||||
|
public static Statistics getInstance() {
|
||||||
|
return sInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Statistics() {
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reset() {
|
||||||
|
mCharCount = 0;
|
||||||
|
mLetterCount = 0;
|
||||||
|
mNumberCount = 0;
|
||||||
|
mSpaceCount = 0;
|
||||||
|
mDeleteKeyCount = 0;
|
||||||
|
mWordCount = 0;
|
||||||
|
mIsEmptyUponStarting = true;
|
||||||
|
mIsEmptinessStateKnown = false;
|
||||||
|
mKeyCounter.reset();
|
||||||
|
mBeforeDeleteKeyCounter.reset();
|
||||||
|
mDuringRepeatedDeleteKeysCounter.reset();
|
||||||
|
mAfterDeleteKeyCounter.reset();
|
||||||
|
|
||||||
|
mLastTapTime = 0;
|
||||||
|
mIsLastKeyDeleteKey = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void recordChar(int codePoint, long time) {
|
||||||
|
final long delta = time - mLastTapTime;
|
||||||
|
if (codePoint == Keyboard.CODE_DELETE) {
|
||||||
|
mDeleteKeyCount++;
|
||||||
|
if (delta < MIN_DELETION_INTERMISSION) {
|
||||||
|
if (mIsLastKeyDeleteKey) {
|
||||||
|
mDuringRepeatedDeleteKeysCounter.add(delta);
|
||||||
|
} else {
|
||||||
|
mBeforeDeleteKeyCounter.add(delta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mIsLastKeyDeleteKey = true;
|
||||||
|
} else {
|
||||||
|
mCharCount++;
|
||||||
|
if (Character.isDigit(codePoint)) {
|
||||||
|
mNumberCount++;
|
||||||
|
}
|
||||||
|
if (Character.isLetter(codePoint)) {
|
||||||
|
mLetterCount++;
|
||||||
|
}
|
||||||
|
if (Character.isSpaceChar(codePoint)) {
|
||||||
|
mSpaceCount++;
|
||||||
|
}
|
||||||
|
if (mIsLastKeyDeleteKey && delta < MIN_DELETION_INTERMISSION) {
|
||||||
|
mAfterDeleteKeyCounter.add(delta);
|
||||||
|
} else if (!mIsLastKeyDeleteKey && delta < MIN_TYPING_INTERMISSION) {
|
||||||
|
mKeyCounter.add(delta);
|
||||||
|
}
|
||||||
|
mIsLastKeyDeleteKey = false;
|
||||||
|
}
|
||||||
|
mLastTapTime = time;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void recordWordEntered() {
|
||||||
|
mWordCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIsEmptyUponStarting(final boolean isEmpty) {
|
||||||
|
mIsEmptyUponStarting = isEmpty;
|
||||||
|
mIsEmptinessStateKnown = true;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue