Merge "[Rlog27] Add replay capability"
commit
4e049897ef
|
@ -428,7 +428,7 @@ public final class LatinIME extends InputMethodService implements KeyboardAction
|
||||||
initSuggest();
|
initSuggest();
|
||||||
|
|
||||||
if (ProductionFlag.IS_EXPERIMENTAL) {
|
if (ProductionFlag.IS_EXPERIMENTAL) {
|
||||||
ResearchLogger.getInstance().init(this);
|
ResearchLogger.getInstance().init(this, mKeyboardSwitcher);
|
||||||
}
|
}
|
||||||
mDisplayOrientation = getResources().getConfiguration().orientation;
|
mDisplayOrientation = getResources().getConfiguration().orientation;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,146 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2013 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A template for typed information stored in the logs.
|
||||||
|
*
|
||||||
|
* A LogStatement contains a name, keys, and flags about whether the {@code Object[] values}
|
||||||
|
* associated with the {@code String[] keys} are likely to reveal information about the user. The
|
||||||
|
* actual values are stored separately.
|
||||||
|
*/
|
||||||
|
class LogStatement {
|
||||||
|
// Constants for particular statements
|
||||||
|
public static final String TYPE_POINTER_TRACKER_CALL_LISTENER_ON_CODE_INPUT =
|
||||||
|
"PointerTrackerCallListenerOnCodeInput";
|
||||||
|
public static final String KEY_CODE = "code";
|
||||||
|
public static final String VALUE_RESEARCH = "research";
|
||||||
|
public static final String TYPE_LATIN_KEYBOARD_VIEW_ON_LONG_PRESS =
|
||||||
|
"LatinKeyboardViewOnLongPress";
|
||||||
|
public static final String ACTION = "action";
|
||||||
|
public static final String VALUE_DOWN = "DOWN";
|
||||||
|
public static final String TYPE_LATIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENTS =
|
||||||
|
"LatinKeyboardViewProcessMotionEvents";
|
||||||
|
public static final String KEY_LOGGING_RELATED = "loggingRelated";
|
||||||
|
|
||||||
|
// Name specifying the LogStatement type.
|
||||||
|
private final String mType;
|
||||||
|
|
||||||
|
// mIsPotentiallyPrivate indicates that event contains potentially private information. If
|
||||||
|
// the word that this event is a part of is determined to be privacy-sensitive, then this
|
||||||
|
// event should not be included in the output log. The system waits to output until the
|
||||||
|
// containing word is known.
|
||||||
|
private final boolean mIsPotentiallyPrivate;
|
||||||
|
|
||||||
|
// mIsPotentiallyRevealing indicates that this statement may disclose details about other
|
||||||
|
// words typed in other LogUnits. This can happen if the user is not inserting spaces, and
|
||||||
|
// data from Suggestions and/or Composing text reveals the entire "megaword". For example,
|
||||||
|
// say the user is typing "for the win", and the system wants to record the bigram "the
|
||||||
|
// win". If the user types "forthe", omitting the space, the system will give "for the" as
|
||||||
|
// a suggestion. If the user accepts the autocorrection, the suggestion for "for the" is
|
||||||
|
// included in the log for the word "the", disclosing that the previous word had been "for".
|
||||||
|
// For now, we simply do not include this data when logging part of a "megaword".
|
||||||
|
private final boolean mIsPotentiallyRevealing;
|
||||||
|
|
||||||
|
// mKeys stores the names that are the attributes in the output json objects
|
||||||
|
private final String[] mKeys;
|
||||||
|
private static final String[] NULL_KEYS = new String[0];
|
||||||
|
|
||||||
|
LogStatement(final String name, final boolean isPotentiallyPrivate,
|
||||||
|
final boolean isPotentiallyRevealing, final String... keys) {
|
||||||
|
mType = name;
|
||||||
|
mIsPotentiallyPrivate = isPotentiallyPrivate;
|
||||||
|
mIsPotentiallyRevealing = isPotentiallyRevealing;
|
||||||
|
mKeys = (keys == null) ? NULL_KEYS : keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getType() {
|
||||||
|
return mType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPotentiallyPrivate() {
|
||||||
|
return mIsPotentiallyPrivate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPotentiallyRevealing() {
|
||||||
|
return mIsPotentiallyRevealing;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String[] getKeys() {
|
||||||
|
return mKeys;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function to test whether a key-value pair exists in a LogStatement.
|
||||||
|
*
|
||||||
|
* A LogStatement is really just a template -- it does not contain the values, only the
|
||||||
|
* keys. So the values must be passed in as an argument.
|
||||||
|
*
|
||||||
|
* @param queryKey the String that is tested by {@code String.equals()} to the keys in the
|
||||||
|
* LogStatement
|
||||||
|
* @param queryValue an Object that must be {@code Object.equals()} to the key's corresponding
|
||||||
|
* value in the {@code values} array
|
||||||
|
* @param values the values corresponding to mKeys
|
||||||
|
*
|
||||||
|
* @returns {@true} if {@code queryKey} exists in the keys for this LogStatement, and {@code
|
||||||
|
* queryValue} matches the corresponding value in {@code values}
|
||||||
|
*
|
||||||
|
* @throws IllegalArgumentException if {@code values.length} is not equal to keys().length()
|
||||||
|
*/
|
||||||
|
public boolean containsKeyValuePair(final String queryKey, final Object queryValue,
|
||||||
|
final Object[] values) {
|
||||||
|
if (mKeys.length != values.length) {
|
||||||
|
throw new IllegalArgumentException("Mismatched number of keys and values.");
|
||||||
|
}
|
||||||
|
final int length = mKeys.length;
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
if (mKeys[i].equals(queryKey) && values[i].equals(queryValue)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function to set a value in a LogStatement.
|
||||||
|
*
|
||||||
|
* A LogStatement is really just a template -- it does not contain the values, only the
|
||||||
|
* keys. So the values must be passed in as an argument.
|
||||||
|
*
|
||||||
|
* @param queryKey the String that is tested by {@code String.equals()} to the keys in the
|
||||||
|
* LogStatement
|
||||||
|
* @param values the array of values corresponding to mKeys
|
||||||
|
* @param newValue the replacement value to go into the {@code values} array
|
||||||
|
*
|
||||||
|
* @returns {@true} if the key exists and the value was successfully set, {@false} otherwise
|
||||||
|
*
|
||||||
|
* @throws IllegalArgumentException if {@code values.length} is not equal to keys().length()
|
||||||
|
*/
|
||||||
|
public boolean setValue(final String queryKey, final Object[] values, final Object newValue) {
|
||||||
|
if (mKeys.length != values.length) {
|
||||||
|
throw new IllegalArgumentException("Mismatched number of keys and values.");
|
||||||
|
}
|
||||||
|
final int length = mKeys.length;
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
if (mKeys[i].equals(queryKey)) {
|
||||||
|
values[i] = newValue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,15 +26,12 @@ import android.view.inputmethod.CompletionInfo;
|
||||||
import com.android.inputmethod.keyboard.Key;
|
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.Utils;
|
|
||||||
import com.android.inputmethod.latin.define.ProductionFlag;
|
import com.android.inputmethod.latin.define.ProductionFlag;
|
||||||
import com.android.inputmethod.research.ResearchLogger.LogStatement;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.StringWriter;
|
import java.io.StringWriter;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A group of log statements related to each other.
|
* A group of log statements related to each other.
|
||||||
|
@ -53,6 +50,7 @@ import java.util.Map;
|
||||||
/* package */ class LogUnit {
|
/* package */ class LogUnit {
|
||||||
private static final String TAG = LogUnit.class.getSimpleName();
|
private static final String TAG = LogUnit.class.getSimpleName();
|
||||||
private static final boolean DEBUG = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG;
|
private static final boolean DEBUG = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG;
|
||||||
|
|
||||||
private final ArrayList<LogStatement> mLogStatementList;
|
private final ArrayList<LogStatement> mLogStatementList;
|
||||||
private final ArrayList<Object[]> mValuesList;
|
private final ArrayList<Object[]> mValuesList;
|
||||||
// Assume that mTimeList is sorted in increasing order. Do not insert null values into
|
// Assume that mTimeList is sorted in increasing order. Do not insert null values into
|
||||||
|
@ -142,10 +140,10 @@ import java.util.Map;
|
||||||
JsonWriter jsonWriter = null;
|
JsonWriter jsonWriter = null;
|
||||||
for (int i = 0; i < size; i++) {
|
for (int i = 0; i < size; i++) {
|
||||||
final LogStatement logStatement = mLogStatementList.get(i);
|
final LogStatement logStatement = mLogStatementList.get(i);
|
||||||
if (!canIncludePrivateData && logStatement.mIsPotentiallyPrivate) {
|
if (!canIncludePrivateData && logStatement.isPotentiallyPrivate()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (mIsPartOfMegaword && logStatement.mIsPotentiallyRevealing) {
|
if (mIsPartOfMegaword && logStatement.isPotentiallyRevealing()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Only retrieve the jsonWriter if we need to. If we don't get this far, then
|
// Only retrieve the jsonWriter if we need to. If we don't get this far, then
|
||||||
|
@ -228,16 +226,16 @@ import java.util.Map;
|
||||||
private boolean outputLogStatementToLocked(final JsonWriter jsonWriter,
|
private boolean outputLogStatementToLocked(final JsonWriter jsonWriter,
|
||||||
final LogStatement logStatement, final Object[] values, final Long time) {
|
final LogStatement logStatement, final Object[] values, final Long time) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
if (logStatement.mKeys.length != values.length) {
|
if (logStatement.getKeys().length != values.length) {
|
||||||
Log.d(TAG, "Key and Value list sizes do not match. " + logStatement.mName);
|
Log.d(TAG, "Key and Value list sizes do not match. " + logStatement.getType());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
jsonWriter.beginObject();
|
jsonWriter.beginObject();
|
||||||
jsonWriter.name(CURRENT_TIME_KEY).value(System.currentTimeMillis());
|
jsonWriter.name(CURRENT_TIME_KEY).value(System.currentTimeMillis());
|
||||||
jsonWriter.name(UPTIME_KEY).value(time);
|
jsonWriter.name(UPTIME_KEY).value(time);
|
||||||
jsonWriter.name(EVENT_TYPE_KEY).value(logStatement.mName);
|
jsonWriter.name(EVENT_TYPE_KEY).value(logStatement.getType());
|
||||||
final String[] keys = logStatement.mKeys;
|
final String[] keys = logStatement.getKeys();
|
||||||
final int length = values.length;
|
final int length = values.length;
|
||||||
for (int i = 0; i < length; i++) {
|
for (int i = 0; i < length; i++) {
|
||||||
jsonWriter.name(keys[i]);
|
jsonWriter.name(keys[i]);
|
||||||
|
@ -261,8 +259,8 @@ import java.util.Map;
|
||||||
} else if (value == null) {
|
} else if (value == null) {
|
||||||
jsonWriter.nullValue();
|
jsonWriter.nullValue();
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "Unrecognized type to be logged: " +
|
Log.w(TAG, "Unrecognized type to be logged: "
|
||||||
(value == null ? "<null>" : value.getClass().getName()));
|
+ (value == null ? "<null>" : value.getClass().getName()));
|
||||||
jsonWriter.nullValue();
|
jsonWriter.nullValue();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -422,4 +420,123 @@ import java.util.Map;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove data associated with selecting the Research button.
|
||||||
|
*
|
||||||
|
* A LogUnit will capture all user interactions with the IME, including the "meta-interactions"
|
||||||
|
* of using the Research button to control the logging (e.g. by starting and stopping recording
|
||||||
|
* of a test case). Because meta-interactions should not be part of the normal log, calling
|
||||||
|
* this method will set a field in the LogStatements of the motion events to indiciate that
|
||||||
|
* they should be disregarded.
|
||||||
|
*
|
||||||
|
* This implementation assumes that the data recorded by the meta-interaction takes the
|
||||||
|
* form of all events following the first MotionEvent.ACTION_DOWN before the first long-press
|
||||||
|
* before the last onCodeEvent containing a code matching {@code LogStatement.VALUE_RESEARCH}.
|
||||||
|
*
|
||||||
|
* @returns true if data was removed
|
||||||
|
*/
|
||||||
|
public boolean removeResearchButtonInvocation() {
|
||||||
|
// This method is designed to be idempotent.
|
||||||
|
|
||||||
|
// First, find last invocation of "research" key
|
||||||
|
final int indexOfLastResearchKey = findLastIndexContainingKeyValue(
|
||||||
|
LogStatement.TYPE_POINTER_TRACKER_CALL_LISTENER_ON_CODE_INPUT,
|
||||||
|
LogStatement.KEY_CODE, LogStatement.VALUE_RESEARCH);
|
||||||
|
if (indexOfLastResearchKey < 0) {
|
||||||
|
// Could not find invocation of "research" key. Leave log as is.
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "Could not find research key");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for the long press that started the invocation of the research key code input.
|
||||||
|
final int indexOfLastLongPressBeforeResearchKey =
|
||||||
|
findLastIndexBefore(LogStatement.TYPE_LATIN_KEYBOARD_VIEW_ON_LONG_PRESS,
|
||||||
|
indexOfLastResearchKey);
|
||||||
|
|
||||||
|
// Look for DOWN event preceding the long press
|
||||||
|
final int indexOfLastDownEventBeforeLongPress =
|
||||||
|
findLastIndexContainingKeyValueBefore(
|
||||||
|
LogStatement.TYPE_LATIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENTS,
|
||||||
|
LogStatement.ACTION, LogStatement.VALUE_DOWN,
|
||||||
|
indexOfLastLongPressBeforeResearchKey);
|
||||||
|
|
||||||
|
// Flag all LatinKeyboardViewProcessMotionEvents from the DOWN event to the research key as
|
||||||
|
// logging-related
|
||||||
|
final int startingIndex = indexOfLastDownEventBeforeLongPress == -1 ? 0
|
||||||
|
: indexOfLastDownEventBeforeLongPress;
|
||||||
|
for (int index = startingIndex; index < indexOfLastResearchKey; index++) {
|
||||||
|
final LogStatement logStatement = mLogStatementList.get(index);
|
||||||
|
final String type = logStatement.getType();
|
||||||
|
final Object[] values = mValuesList.get(index);
|
||||||
|
if (type.equals(LogStatement.TYPE_LATIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENTS)) {
|
||||||
|
logStatement.setValue(LogStatement.KEY_LOGGING_RELATED, values, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the index of the last LogStatement before {@code startingIndex} of type {@code type}.
|
||||||
|
*
|
||||||
|
* @param queryType a String that must be {@code String.equals()} to the LogStatement type
|
||||||
|
* @param startingIndex the index to start the backward search from. Must be less than the
|
||||||
|
* length of mLogStatementList, or an IndexOutOfBoundsException is thrown. Can be negative,
|
||||||
|
* in which case -1 is returned.
|
||||||
|
*
|
||||||
|
* @return The index of the last LogStatement, -1 if none exists.
|
||||||
|
*/
|
||||||
|
private int findLastIndexBefore(final String queryType, final int startingIndex) {
|
||||||
|
return findLastIndexContainingKeyValueBefore(queryType, null, null, startingIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the index of the last LogStatement before {@code startingIndex} of type {@code type}
|
||||||
|
* containing the given key-value pair.
|
||||||
|
*
|
||||||
|
* @param queryType a String that must be {@code String.equals()} to the LogStatement type
|
||||||
|
* @param queryKey a String that must be {@code String.equals()} to a key in the LogStatement
|
||||||
|
* @param queryValue an Object that must be {@code String.equals()} to the key's corresponding
|
||||||
|
* value
|
||||||
|
*
|
||||||
|
* @return The index of the last LogStatement, -1 if none exists.
|
||||||
|
*/
|
||||||
|
private int findLastIndexContainingKeyValue(final String queryType, final String queryKey,
|
||||||
|
final Object queryValue) {
|
||||||
|
return findLastIndexContainingKeyValueBefore(queryType, queryKey, queryValue,
|
||||||
|
mLogStatementList.size() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the index of the last LogStatement before {@code startingIndex} of type {@code type}
|
||||||
|
* containing the given key-value pair.
|
||||||
|
*
|
||||||
|
* @param queryType a String that must be {@code String.equals()} to the LogStatement type
|
||||||
|
* @param queryKey a String that must be {@code String.equals()} to a key in the LogStatement
|
||||||
|
* @param queryValue an Object that must be {@code String.equals()} to the key's corresponding
|
||||||
|
* value
|
||||||
|
* @param startingIndex the index to start the backward search from. Must be less than the
|
||||||
|
* length of mLogStatementList, or an IndexOutOfBoundsException is thrown. Can be negative,
|
||||||
|
* in which case -1 is returned.
|
||||||
|
*
|
||||||
|
* @return The index of the last LogStatement, -1 if none exists.
|
||||||
|
*/
|
||||||
|
private int findLastIndexContainingKeyValueBefore(final String queryType, final String queryKey,
|
||||||
|
final Object queryValue, final int startingIndex) {
|
||||||
|
if (startingIndex < 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
for (int index = startingIndex; index >= 0; index--) {
|
||||||
|
final LogStatement logStatement = mLogStatementList.get(index);
|
||||||
|
final String type = logStatement.getType();
|
||||||
|
if (type.equals(queryType) && (queryKey == null
|
||||||
|
|| logStatement.containsKeyValuePair(queryKey, queryValue,
|
||||||
|
mValuesList.get(index)))) {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,113 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2013 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 android.util.JsonReader;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
|
||||||
|
import com.android.inputmethod.latin.define.ProductionFlag;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
public class MotionEventReader {
|
||||||
|
private static final String TAG = MotionEventReader.class.getSimpleName();
|
||||||
|
private static final boolean DEBUG = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG;
|
||||||
|
|
||||||
|
public ReplayData readMotionEventData(final File file) {
|
||||||
|
final ReplayData replayData = new ReplayData();
|
||||||
|
try {
|
||||||
|
// Read file
|
||||||
|
final JsonReader jsonReader = new JsonReader(new BufferedReader(new InputStreamReader(
|
||||||
|
new FileInputStream(file))));
|
||||||
|
jsonReader.beginArray();
|
||||||
|
while (jsonReader.hasNext()) {
|
||||||
|
readLogStatement(jsonReader, replayData);
|
||||||
|
}
|
||||||
|
jsonReader.endArray();
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return replayData;
|
||||||
|
}
|
||||||
|
|
||||||
|
static class ReplayData {
|
||||||
|
final ArrayList<Integer> mActions = new ArrayList<Integer>();
|
||||||
|
final ArrayList<Integer> mXCoords = new ArrayList<Integer>();
|
||||||
|
final ArrayList<Integer> mYCoords = new ArrayList<Integer>();
|
||||||
|
final ArrayList<Long> mTimes = new ArrayList<Long>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void readLogStatement(final JsonReader jsonReader, final ReplayData replayData)
|
||||||
|
throws IOException {
|
||||||
|
String logStatementType = null;
|
||||||
|
Integer actionType = null;
|
||||||
|
Integer x = null;
|
||||||
|
Integer y = null;
|
||||||
|
Long time = null;
|
||||||
|
boolean loggingRelated = false;
|
||||||
|
|
||||||
|
jsonReader.beginObject();
|
||||||
|
while (jsonReader.hasNext()) {
|
||||||
|
final String key = jsonReader.nextName();
|
||||||
|
if (key.equals("_ty")) {
|
||||||
|
logStatementType = jsonReader.nextString();
|
||||||
|
} else if (key.equals("_ut")) {
|
||||||
|
time = jsonReader.nextLong();
|
||||||
|
} else if (key.equals("x")) {
|
||||||
|
x = jsonReader.nextInt();
|
||||||
|
} else if (key.equals("y")) {
|
||||||
|
y = jsonReader.nextInt();
|
||||||
|
} else if (key.equals("action")) {
|
||||||
|
final String s = jsonReader.nextString();
|
||||||
|
if (s.equals("UP")) {
|
||||||
|
actionType = MotionEvent.ACTION_UP;
|
||||||
|
} else if (s.equals("DOWN")) {
|
||||||
|
actionType = MotionEvent.ACTION_DOWN;
|
||||||
|
} else if (s.equals("MOVE")) {
|
||||||
|
actionType = MotionEvent.ACTION_MOVE;
|
||||||
|
}
|
||||||
|
} else if (key.equals("loggingRelated")) {
|
||||||
|
loggingRelated = jsonReader.nextBoolean();
|
||||||
|
} else {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.w(TAG, "Unknown JSON key in LogStatement: " + key);
|
||||||
|
}
|
||||||
|
jsonReader.skipValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jsonReader.endObject();
|
||||||
|
|
||||||
|
if (logStatementType != null && time != null && x != null && y != null && actionType != null
|
||||||
|
&& logStatementType.equals("MainKeyboardViewProcessMotionEvent")
|
||||||
|
&& !loggingRelated) {
|
||||||
|
replayData.mActions.add(actionType);
|
||||||
|
replayData.mXCoords.add(x);
|
||||||
|
replayData.mYCoords.add(y);
|
||||||
|
replayData.mTimes.add(time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,120 @@
|
||||||
|
/*
|
||||||
|
* 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 android.os.Handler;
|
||||||
|
import android.os.Message;
|
||||||
|
import android.os.SystemClock;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
|
||||||
|
import com.android.inputmethod.keyboard.KeyboardSwitcher;
|
||||||
|
import com.android.inputmethod.keyboard.MainKeyboardView;
|
||||||
|
import com.android.inputmethod.latin.define.ProductionFlag;
|
||||||
|
import com.android.inputmethod.research.MotionEventReader.ReplayData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replays a sequence of motion events in realtime on the screen.
|
||||||
|
*
|
||||||
|
* Useful for user inspection of logged data.
|
||||||
|
*/
|
||||||
|
public class Replayer {
|
||||||
|
private static final String TAG = Replayer.class.getSimpleName();
|
||||||
|
private static final boolean DEBUG = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG;
|
||||||
|
private static final long START_TIME_DELAY_MS = 500;
|
||||||
|
|
||||||
|
private boolean mIsReplaying = false;
|
||||||
|
private KeyboardSwitcher mKeyboardSwitcher;
|
||||||
|
|
||||||
|
public void setKeyboardSwitcher(final KeyboardSwitcher keyboardSwitcher) {
|
||||||
|
mKeyboardSwitcher = keyboardSwitcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final int MSG_MOTION_EVENT = 0;
|
||||||
|
private static final int MSG_DONE = 1;
|
||||||
|
private static final int COMPLETION_TIME_MS = 500;
|
||||||
|
|
||||||
|
// TODO: Support historical events and multi-touch.
|
||||||
|
public void replay(final ReplayData replayData) {
|
||||||
|
if (mIsReplaying) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mIsReplaying = true;
|
||||||
|
final int numActions = replayData.mActions.size();
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "replaying " + numActions + " actions");
|
||||||
|
}
|
||||||
|
if (numActions == 0) {
|
||||||
|
mIsReplaying = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
|
||||||
|
|
||||||
|
// The reference time relative to the times stored in events.
|
||||||
|
final long origStartTime = replayData.mTimes.get(0);
|
||||||
|
// The reference time relative to which events are replayed in the present.
|
||||||
|
final long currentStartTime = SystemClock.uptimeMillis() + START_TIME_DELAY_MS;
|
||||||
|
// The adjustment needed to translate times from the original recorded time to the current
|
||||||
|
// time.
|
||||||
|
final long timeAdjustment = currentStartTime - origStartTime;
|
||||||
|
final Handler handler = new Handler() {
|
||||||
|
// Track the time of the most recent DOWN event, to be passed as a parameter when
|
||||||
|
// constructing a MotionEvent. It's initialized here to the origStartTime, but this is
|
||||||
|
// only a precaution. The value should be overwritten by the first ACTION_DOWN event
|
||||||
|
// before the first use of the variable. Note that this may cause the first few events
|
||||||
|
// to have incorrect {@code downTime}s.
|
||||||
|
private long mOrigDownTime = origStartTime;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleMessage(final Message msg) {
|
||||||
|
switch (msg.what) {
|
||||||
|
case MSG_MOTION_EVENT:
|
||||||
|
final int index = msg.arg1;
|
||||||
|
final int action = replayData.mActions.get(index);
|
||||||
|
final int x = replayData.mXCoords.get(index);
|
||||||
|
final int y = replayData.mYCoords.get(index);
|
||||||
|
final long origTime = replayData.mTimes.get(index);
|
||||||
|
if (action == MotionEvent.ACTION_DOWN) {
|
||||||
|
mOrigDownTime = origTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
final MotionEvent me = MotionEvent.obtain(mOrigDownTime + timeAdjustment,
|
||||||
|
origTime + timeAdjustment, action, x, y, 0);
|
||||||
|
mainKeyboardView.processMotionEvent(me);
|
||||||
|
me.recycle();
|
||||||
|
break;
|
||||||
|
case MSG_DONE:
|
||||||
|
mIsReplaying = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (int i = 0; i < numActions; i++) {
|
||||||
|
final Message msg = Message.obtain(handler, MSG_MOTION_EVENT, i, 0);
|
||||||
|
final long msgTime = replayData.mTimes.get(i) + timeAdjustment;
|
||||||
|
handler.sendMessageAtTime(msg, msgTime);
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "queuing event at " + msgTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final long presentDoneTime = replayData.mTimes.get(numActions - 1) + timeAdjustment
|
||||||
|
+ COMPLETION_TIME_MS;
|
||||||
|
handler.sendMessageAtTime(Message.obtain(handler, MSG_DONE), presentDoneTime);
|
||||||
|
}
|
||||||
|
}
|
|
@ -57,6 +57,7 @@ import android.widget.Toast;
|
||||||
import com.android.inputmethod.keyboard.Key;
|
import com.android.inputmethod.keyboard.Key;
|
||||||
import com.android.inputmethod.keyboard.Keyboard;
|
import com.android.inputmethod.keyboard.Keyboard;
|
||||||
import com.android.inputmethod.keyboard.KeyboardId;
|
import com.android.inputmethod.keyboard.KeyboardId;
|
||||||
|
import com.android.inputmethod.keyboard.KeyboardSwitcher;
|
||||||
import com.android.inputmethod.keyboard.KeyboardView;
|
import com.android.inputmethod.keyboard.KeyboardView;
|
||||||
import com.android.inputmethod.keyboard.MainKeyboardView;
|
import com.android.inputmethod.keyboard.MainKeyboardView;
|
||||||
import com.android.inputmethod.latin.Constants;
|
import com.android.inputmethod.latin.Constants;
|
||||||
|
@ -98,8 +99,10 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
private static final int OUTPUT_FORMAT_VERSION = 5;
|
private static final int OUTPUT_FORMAT_VERSION = 5;
|
||||||
private static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode";
|
private static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode";
|
||||||
private static final String PREF_RESEARCH_HAS_SEEN_SPLASH = "pref_research_has_seen_splash";
|
private static final String PREF_RESEARCH_HAS_SEEN_SPLASH = "pref_research_has_seen_splash";
|
||||||
/* package */ static final String FILENAME_PREFIX = "researchLog";
|
/* package */ static final String LOG_FILENAME_PREFIX = "researchLog";
|
||||||
private static final String FILENAME_SUFFIX = ".txt";
|
private static final String LOG_FILENAME_SUFFIX = ".txt";
|
||||||
|
/* package */ static final String USER_RECORDING_FILENAME_PREFIX = "recording";
|
||||||
|
private static final String USER_RECORDING_FILENAME_SUFFIX = ".txt";
|
||||||
private static final SimpleDateFormat TIMESTAMP_DATEFORMAT =
|
private static final SimpleDateFormat TIMESTAMP_DATEFORMAT =
|
||||||
new SimpleDateFormat("yyyyMMddHHmmssS", Locale.US);
|
new SimpleDateFormat("yyyyMMddHHmmssS", Locale.US);
|
||||||
// Whether to show an indicator on the screen that logging is on. Currently a very small red
|
// Whether to show an indicator on the screen that logging is on. Currently a very small red
|
||||||
|
@ -129,9 +132,15 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
// the system to do so.
|
// the system to do so.
|
||||||
// LogUnits are queued in the LogBuffers and published to the ResearchLogs when words are
|
// LogUnits are queued in the LogBuffers and published to the ResearchLogs when words are
|
||||||
// complete.
|
// complete.
|
||||||
/* package */ ResearchLog mFeedbackLog;
|
|
||||||
/* package */ MainLogBuffer mMainLogBuffer;
|
/* package */ MainLogBuffer mMainLogBuffer;
|
||||||
|
// TODO: Remove the feedback log. The feedback log continuously captured user data in case the
|
||||||
|
// user wanted to submit it. We now use the mUserRecordingLogBuffer to allow the user to
|
||||||
|
// explicitly reproduce a problem.
|
||||||
|
/* package */ ResearchLog mFeedbackLog;
|
||||||
/* package */ LogBuffer mFeedbackLogBuffer;
|
/* package */ LogBuffer mFeedbackLogBuffer;
|
||||||
|
/* package */ ResearchLog mUserRecordingLog;
|
||||||
|
/* package */ LogBuffer mUserRecordingLogBuffer;
|
||||||
|
private File mUserRecordingFile = null;
|
||||||
|
|
||||||
private boolean mIsPasswordView = false;
|
private boolean mIsPasswordView = false;
|
||||||
private boolean mIsLoggingSuspended = false;
|
private boolean mIsLoggingSuspended = false;
|
||||||
|
@ -155,6 +164,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
private MainKeyboardView mMainKeyboardView;
|
private MainKeyboardView mMainKeyboardView;
|
||||||
private LatinIME mLatinIME;
|
private LatinIME mLatinIME;
|
||||||
private final Statistics mStatistics;
|
private final Statistics mStatistics;
|
||||||
|
private final MotionEventReader mMotionEventReader = new MotionEventReader();
|
||||||
|
private final Replayer mReplayer = new Replayer();
|
||||||
|
|
||||||
private Intent mUploadIntent;
|
private Intent mUploadIntent;
|
||||||
|
|
||||||
|
@ -173,7 +184,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
return sInstance;
|
return sInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void init(final LatinIME latinIME) {
|
public void init(final LatinIME latinIME, final KeyboardSwitcher keyboardSwitcher) {
|
||||||
assert latinIME != null;
|
assert latinIME != null;
|
||||||
if (latinIME == null) {
|
if (latinIME == null) {
|
||||||
Log.w(TAG, "IMS is null; logging is off");
|
Log.w(TAG, "IMS is null; logging is off");
|
||||||
|
@ -210,6 +221,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
mLatinIME = latinIME;
|
mLatinIME = latinIME;
|
||||||
mPrefs = prefs;
|
mPrefs = prefs;
|
||||||
mUploadIntent = new Intent(mLatinIME, UploaderService.class);
|
mUploadIntent = new Intent(mLatinIME, UploaderService.class);
|
||||||
|
mReplayer.setKeyboardSwitcher(keyboardSwitcher);
|
||||||
|
|
||||||
if (ProductionFlag.IS_EXPERIMENTAL) {
|
if (ProductionFlag.IS_EXPERIMENTAL) {
|
||||||
scheduleUploadingService(mLatinIME);
|
scheduleUploadingService(mLatinIME);
|
||||||
|
@ -237,8 +249,10 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
|
|
||||||
private void cleanupLoggingDir(final File dir, final long time) {
|
private void cleanupLoggingDir(final File dir, final long time) {
|
||||||
for (File file : dir.listFiles()) {
|
for (File file : dir.listFiles()) {
|
||||||
if (file.getName().startsWith(ResearchLogger.FILENAME_PREFIX) &&
|
final String filename = file.getName();
|
||||||
file.lastModified() < time) {
|
if ((filename.startsWith(ResearchLogger.LOG_FILENAME_PREFIX)
|
||||||
|
|| filename.startsWith(ResearchLogger.USER_RECORDING_FILENAME_PREFIX))
|
||||||
|
&& file.lastModified() < time) {
|
||||||
file.delete();
|
file.delete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -335,9 +349,9 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
|
|
||||||
private static int sLogFileCounter = 0;
|
private static int sLogFileCounter = 0;
|
||||||
|
|
||||||
private File createLogFile(File filesDir) {
|
private File createLogFile(final File filesDir) {
|
||||||
final StringBuilder sb = new StringBuilder();
|
final StringBuilder sb = new StringBuilder();
|
||||||
sb.append(FILENAME_PREFIX).append('-');
|
sb.append(LOG_FILENAME_PREFIX).append('-');
|
||||||
sb.append(mUUIDString).append('-');
|
sb.append(mUUIDString).append('-');
|
||||||
sb.append(TIMESTAMP_DATEFORMAT.format(new Date())).append('-');
|
sb.append(TIMESTAMP_DATEFORMAT.format(new Date())).append('-');
|
||||||
// Sometimes logFiles are created within milliseconds of each other. Append a counter to
|
// Sometimes logFiles are created within milliseconds of each other. Append a counter to
|
||||||
|
@ -349,7 +363,16 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
sLogFileCounter = 0;
|
sLogFileCounter = 0;
|
||||||
}
|
}
|
||||||
sb.append(sLogFileCounter);
|
sb.append(sLogFileCounter);
|
||||||
sb.append(FILENAME_SUFFIX);
|
sb.append(LOG_FILENAME_SUFFIX);
|
||||||
|
return new File(filesDir, sb.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private File createUserRecordingFile(final File filesDir) {
|
||||||
|
final StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append(USER_RECORDING_FILENAME_PREFIX).append('-');
|
||||||
|
sb.append(mUUIDString).append('-');
|
||||||
|
sb.append(TIMESTAMP_DATEFORMAT.format(new Date()));
|
||||||
|
sb.append(USER_RECORDING_FILENAME_SUFFIX);
|
||||||
return new File(filesDir, sb.toString());
|
return new File(filesDir, sb.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -517,37 +540,32 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
presentFeedbackDialog(latinIME);
|
presentFeedbackDialog(latinIME);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: currently unreachable. Remove after being sure no menu is needed.
|
private void cancelRecording() {
|
||||||
/*
|
if (mUserRecordingLog != null) {
|
||||||
public void presentResearchDialog(final LatinIME latinIME) {
|
mUserRecordingLog.abort();
|
||||||
final CharSequence title = latinIME.getString(R.string.english_ime_research_log);
|
|
||||||
final boolean showEnable = mIsLoggingSuspended || !sIsLogging;
|
|
||||||
final CharSequence[] items = new CharSequence[] {
|
|
||||||
latinIME.getString(R.string.research_feedback_menu_option),
|
|
||||||
showEnable ? latinIME.getString(R.string.research_enable_session_logging) :
|
|
||||||
latinIME.getString(R.string.research_do_not_log_this_session)
|
|
||||||
};
|
|
||||||
final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(DialogInterface di, int position) {
|
|
||||||
di.dismiss();
|
|
||||||
switch (position) {
|
|
||||||
case 0:
|
|
||||||
presentFeedbackDialog(latinIME);
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
enableOrDisable(showEnable, latinIME);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
mUserRecordingLog = null;
|
||||||
|
mUserRecordingLogBuffer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
private void startRecording() {
|
||||||
final AlertDialog.Builder builder = new AlertDialog.Builder(latinIME)
|
// Don't record the "start recording" motion.
|
||||||
.setItems(items, listener)
|
commitCurrentLogUnit();
|
||||||
.setTitle(title);
|
if (mUserRecordingLog != null) {
|
||||||
latinIME.showOptionDialog(builder.create());
|
mUserRecordingLog.abort();
|
||||||
|
}
|
||||||
|
mUserRecordingFile = createUserRecordingFile(mFilesDir);
|
||||||
|
mUserRecordingLog = new ResearchLog(mUserRecordingFile, mLatinIME);
|
||||||
|
mUserRecordingLogBuffer = new LogBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveRecording() {
|
||||||
|
commitCurrentLogUnit();
|
||||||
|
publishLogBuffer(mUserRecordingLogBuffer, mUserRecordingLog, true);
|
||||||
|
mUserRecordingLog.close(null);
|
||||||
|
mUserRecordingLog = null;
|
||||||
|
mUserRecordingLogBuffer = null;
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
private boolean mInFeedbackDialog = false;
|
private boolean mInFeedbackDialog = false;
|
||||||
|
|
||||||
|
@ -631,38 +649,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static class LogStatement {
|
|
||||||
final String mName;
|
|
||||||
|
|
||||||
// mIsPotentiallyPrivate indicates that event contains potentially private information. If
|
|
||||||
// the word that this event is a part of is determined to be privacy-sensitive, then this
|
|
||||||
// event should not be included in the output log. The system waits to output until the
|
|
||||||
// containing word is known.
|
|
||||||
final boolean mIsPotentiallyPrivate;
|
|
||||||
|
|
||||||
// mIsPotentiallyRevealing indicates that this statement may disclose details about other
|
|
||||||
// words typed in other LogUnits. This can happen if the user is not inserting spaces, and
|
|
||||||
// data from Suggestions and/or Composing text reveals the entire "megaword". For example,
|
|
||||||
// say the user is typing "for the win", and the system wants to record the bigram "the
|
|
||||||
// win". If the user types "forthe", omitting the space, the system will give "for the" as
|
|
||||||
// a suggestion. If the user accepts the autocorrection, the suggestion for "for the" is
|
|
||||||
// included in the log for the word "the", disclosing that the previous word had been "for".
|
|
||||||
// For now, we simply do not include this data when logging part of a "megaword".
|
|
||||||
final boolean mIsPotentiallyRevealing;
|
|
||||||
|
|
||||||
// mKeys stores the names that are the attributes in the output json objects
|
|
||||||
final String[] mKeys;
|
|
||||||
private static final String[] NULL_KEYS = new String[0];
|
|
||||||
|
|
||||||
LogStatement(final String name, final boolean isPotentiallyPrivate,
|
|
||||||
final boolean isPotentiallyRevealing, final String... keys) {
|
|
||||||
mName = name;
|
|
||||||
mIsPotentiallyPrivate = isPotentiallyPrivate;
|
|
||||||
mIsPotentiallyRevealing = isPotentiallyRevealing;
|
|
||||||
mKeys = (keys == null) ? NULL_KEYS : keys;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final LogStatement LOGSTATEMENT_FEEDBACK =
|
private static final LogStatement LOGSTATEMENT_FEEDBACK =
|
||||||
new LogStatement("UserFeedback", false, false, "contents", "accountName");
|
new LogStatement("UserFeedback", false, false, "contents", "accountName");
|
||||||
public void sendFeedback(final String feedbackContents, final boolean includeHistory,
|
public void sendFeedback(final String feedbackContents, final boolean includeHistory,
|
||||||
|
@ -770,7 +756,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
|
|
||||||
private synchronized void enqueueEvent(final LogUnit logUnit, final LogStatement logStatement,
|
private synchronized void enqueueEvent(final LogUnit logUnit, final LogStatement logStatement,
|
||||||
final Object... values) {
|
final Object... values) {
|
||||||
assert values.length == logStatement.mKeys.length;
|
assert values.length == logStatement.getKeys().length;
|
||||||
if (isAllowedToLog() && logUnit != null) {
|
if (isAllowedToLog() && logUnit != null) {
|
||||||
final long time = SystemClock.uptimeMillis();
|
final long time = SystemClock.uptimeMillis();
|
||||||
logUnit.addLogStatement(logStatement, time, values);
|
logUnit.addLogStatement(logStatement, time, values);
|
||||||
|
@ -801,6 +787,9 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
if (mFeedbackLogBuffer != null) {
|
if (mFeedbackLogBuffer != null) {
|
||||||
mFeedbackLogBuffer.shiftIn(mCurrentLogUnit);
|
mFeedbackLogBuffer.shiftIn(mCurrentLogUnit);
|
||||||
}
|
}
|
||||||
|
if (mUserRecordingLogBuffer != null) {
|
||||||
|
mUserRecordingLogBuffer.shiftIn(mCurrentLogUnit);
|
||||||
|
}
|
||||||
mCurrentLogUnit = new LogUnit();
|
mCurrentLogUnit = new LogUnit();
|
||||||
} else {
|
} else {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
|
@ -1058,7 +1047,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
private static final LogStatement LOGSTATEMENT_MAIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENT =
|
private static final LogStatement LOGSTATEMENT_MAIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENT =
|
||||||
new LogStatement("MotionEvent", true, false, "action", "MotionEvent");
|
new LogStatement("MotionEvent", true, false, "action", "MotionEvent", "loggingRelated");
|
||||||
public static void mainKeyboardView_processMotionEvent(final MotionEvent me, final int action,
|
public static void mainKeyboardView_processMotionEvent(final MotionEvent me, final int action,
|
||||||
final long eventTime, final int index, final int id, final int x, final int y) {
|
final long eventTime, final int index, final int id, final int x, final int y) {
|
||||||
if (me != null) {
|
if (me != null) {
|
||||||
|
@ -1075,7 +1064,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
}
|
}
|
||||||
final ResearchLogger researchLogger = getInstance();
|
final ResearchLogger researchLogger = getInstance();
|
||||||
researchLogger.enqueueEvent(LOGSTATEMENT_MAIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENT,
|
researchLogger.enqueueEvent(LOGSTATEMENT_MAIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENT,
|
||||||
actionString, MotionEvent.obtain(me));
|
actionString, MotionEvent.obtain(me), false);
|
||||||
if (action == MotionEvent.ACTION_DOWN) {
|
if (action == MotionEvent.ACTION_DOWN) {
|
||||||
// Subtract 1 from eventTime so the down event is included in the later
|
// Subtract 1 from eventTime so the down event is included in the later
|
||||||
// LogUnit, not the earlier (the test is for inequality).
|
// LogUnit, not the earlier (the test is for inequality).
|
||||||
|
@ -1442,12 +1431,20 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
final int code) {
|
final int code) {
|
||||||
if (key != null) {
|
if (key != null) {
|
||||||
String outputText = key.getOutputText();
|
String outputText = key.getOutputText();
|
||||||
getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCODEINPUT,
|
final ResearchLogger researchLogger = getInstance();
|
||||||
|
researchLogger.enqueueEvent(LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCODEINPUT,
|
||||||
Constants.printableCode(scrubDigitFromCodePoint(code)),
|
Constants.printableCode(scrubDigitFromCodePoint(code)),
|
||||||
outputText == null ? null : scrubDigitsFromString(outputText.toString()),
|
outputText == null ? null : scrubDigitsFromString(outputText.toString()),
|
||||||
x, y, ignoreModifierKey, altersCode, key.isEnabled());
|
x, y, ignoreModifierKey, altersCode, key.isEnabled());
|
||||||
|
if (code == Constants.CODE_RESEARCH) {
|
||||||
|
researchLogger.suppressResearchKeyMotionData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void suppressResearchKeyMotionData() {
|
||||||
|
mCurrentLogUnit.removeResearchButtonInvocation();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log a call to PointerTracker.callListenerCallListenerOnRelease().
|
* Log a call to PointerTracker.callListenerCallListenerOnRelease().
|
||||||
|
|
|
@ -131,7 +131,7 @@ public final class UploaderService extends IntentService {
|
||||||
final File[] files = mFilesDir.listFiles(new FileFilter() {
|
final File[] files = mFilesDir.listFiles(new FileFilter() {
|
||||||
@Override
|
@Override
|
||||||
public boolean accept(File pathname) {
|
public boolean accept(File pathname) {
|
||||||
return pathname.getName().startsWith(ResearchLogger.FILENAME_PREFIX)
|
return pathname.getName().startsWith(ResearchLogger.LOG_FILENAME_PREFIX)
|
||||||
&& !pathname.canWrite();
|
&& !pathname.canWrite();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue