Merge "[Rlog27] Add replay capability"
commit
4e049897ef
|
@ -428,7 +428,7 @@ public final class LatinIME extends InputMethodService implements KeyboardAction
|
|||
initSuggest();
|
||||
|
||||
if (ProductionFlag.IS_EXPERIMENTAL) {
|
||||
ResearchLogger.getInstance().init(this);
|
||||
ResearchLogger.getInstance().init(this, mKeyboardSwitcher);
|
||||
}
|
||||
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.latin.SuggestedWords;
|
||||
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
|
||||
import com.android.inputmethod.latin.Utils;
|
||||
import com.android.inputmethod.latin.define.ProductionFlag;
|
||||
import com.android.inputmethod.research.ResearchLogger.LogStatement;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A group of log statements related to each other.
|
||||
|
@ -53,6 +50,7 @@ import java.util.Map;
|
|||
/* package */ class LogUnit {
|
||||
private static final String TAG = LogUnit.class.getSimpleName();
|
||||
private static final boolean DEBUG = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG;
|
||||
|
||||
private final ArrayList<LogStatement> mLogStatementList;
|
||||
private final ArrayList<Object[]> mValuesList;
|
||||
// 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;
|
||||
for (int i = 0; i < size; i++) {
|
||||
final LogStatement logStatement = mLogStatementList.get(i);
|
||||
if (!canIncludePrivateData && logStatement.mIsPotentiallyPrivate) {
|
||||
if (!canIncludePrivateData && logStatement.isPotentiallyPrivate()) {
|
||||
continue;
|
||||
}
|
||||
if (mIsPartOfMegaword && logStatement.mIsPotentiallyRevealing) {
|
||||
if (mIsPartOfMegaword && logStatement.isPotentiallyRevealing()) {
|
||||
continue;
|
||||
}
|
||||
// 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,
|
||||
final LogStatement logStatement, final Object[] values, final Long time) {
|
||||
if (DEBUG) {
|
||||
if (logStatement.mKeys.length != values.length) {
|
||||
Log.d(TAG, "Key and Value list sizes do not match. " + logStatement.mName);
|
||||
if (logStatement.getKeys().length != values.length) {
|
||||
Log.d(TAG, "Key and Value list sizes do not match. " + logStatement.getType());
|
||||
}
|
||||
}
|
||||
try {
|
||||
jsonWriter.beginObject();
|
||||
jsonWriter.name(CURRENT_TIME_KEY).value(System.currentTimeMillis());
|
||||
jsonWriter.name(UPTIME_KEY).value(time);
|
||||
jsonWriter.name(EVENT_TYPE_KEY).value(logStatement.mName);
|
||||
final String[] keys = logStatement.mKeys;
|
||||
jsonWriter.name(EVENT_TYPE_KEY).value(logStatement.getType());
|
||||
final String[] keys = logStatement.getKeys();
|
||||
final int length = values.length;
|
||||
for (int i = 0; i < length; i++) {
|
||||
jsonWriter.name(keys[i]);
|
||||
|
@ -261,8 +259,8 @@ import java.util.Map;
|
|||
} else if (value == null) {
|
||||
jsonWriter.nullValue();
|
||||
} else {
|
||||
Log.w(TAG, "Unrecognized type to be logged: " +
|
||||
(value == null ? "<null>" : value.getClass().getName()));
|
||||
Log.w(TAG, "Unrecognized type to be logged: "
|
||||
+ (value == null ? "<null>" : value.getClass().getName()));
|
||||
jsonWriter.nullValue();
|
||||
}
|
||||
}
|
||||
|
@ -422,4 +420,123 @@ import java.util.Map;
|
|||
}
|
||||
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.Keyboard;
|
||||
import com.android.inputmethod.keyboard.KeyboardId;
|
||||
import com.android.inputmethod.keyboard.KeyboardSwitcher;
|
||||
import com.android.inputmethod.keyboard.KeyboardView;
|
||||
import com.android.inputmethod.keyboard.MainKeyboardView;
|
||||
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 String PREF_USABILITY_STUDY_MODE = "usability_study_mode";
|
||||
private static final String PREF_RESEARCH_HAS_SEEN_SPLASH = "pref_research_has_seen_splash";
|
||||
/* package */ static final String FILENAME_PREFIX = "researchLog";
|
||||
private static final String FILENAME_SUFFIX = ".txt";
|
||||
/* package */ static final String LOG_FILENAME_PREFIX = "researchLog";
|
||||
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 =
|
||||
new SimpleDateFormat("yyyyMMddHHmmssS", Locale.US);
|
||||
// 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.
|
||||
// LogUnits are queued in the LogBuffers and published to the ResearchLogs when words are
|
||||
// complete.
|
||||
/* package */ ResearchLog mFeedbackLog;
|
||||
/* 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 */ ResearchLog mUserRecordingLog;
|
||||
/* package */ LogBuffer mUserRecordingLogBuffer;
|
||||
private File mUserRecordingFile = null;
|
||||
|
||||
private boolean mIsPasswordView = false;
|
||||
private boolean mIsLoggingSuspended = false;
|
||||
|
@ -155,6 +164,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
|||
private MainKeyboardView mMainKeyboardView;
|
||||
private LatinIME mLatinIME;
|
||||
private final Statistics mStatistics;
|
||||
private final MotionEventReader mMotionEventReader = new MotionEventReader();
|
||||
private final Replayer mReplayer = new Replayer();
|
||||
|
||||
private Intent mUploadIntent;
|
||||
|
||||
|
@ -173,7 +184,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
|||
return sInstance;
|
||||
}
|
||||
|
||||
public void init(final LatinIME latinIME) {
|
||||
public void init(final LatinIME latinIME, final KeyboardSwitcher keyboardSwitcher) {
|
||||
assert latinIME != null;
|
||||
if (latinIME == null) {
|
||||
Log.w(TAG, "IMS is null; logging is off");
|
||||
|
@ -210,6 +221,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
|||
mLatinIME = latinIME;
|
||||
mPrefs = prefs;
|
||||
mUploadIntent = new Intent(mLatinIME, UploaderService.class);
|
||||
mReplayer.setKeyboardSwitcher(keyboardSwitcher);
|
||||
|
||||
if (ProductionFlag.IS_EXPERIMENTAL) {
|
||||
scheduleUploadingService(mLatinIME);
|
||||
|
@ -237,8 +249,10 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
|||
|
||||
private void cleanupLoggingDir(final File dir, final long time) {
|
||||
for (File file : dir.listFiles()) {
|
||||
if (file.getName().startsWith(ResearchLogger.FILENAME_PREFIX) &&
|
||||
file.lastModified() < time) {
|
||||
final String filename = file.getName();
|
||||
if ((filename.startsWith(ResearchLogger.LOG_FILENAME_PREFIX)
|
||||
|| filename.startsWith(ResearchLogger.USER_RECORDING_FILENAME_PREFIX))
|
||||
&& file.lastModified() < time) {
|
||||
file.delete();
|
||||
}
|
||||
}
|
||||
|
@ -335,9 +349,9 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
|||
|
||||
private static int sLogFileCounter = 0;
|
||||
|
||||
private File createLogFile(File filesDir) {
|
||||
private File createLogFile(final File filesDir) {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
sb.append(FILENAME_PREFIX).append('-');
|
||||
sb.append(LOG_FILENAME_PREFIX).append('-');
|
||||
sb.append(mUUIDString).append('-');
|
||||
sb.append(TIMESTAMP_DATEFORMAT.format(new Date())).append('-');
|
||||
// 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;
|
||||
}
|
||||
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());
|
||||
}
|
||||
|
||||
|
@ -517,37 +540,32 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
|||
presentFeedbackDialog(latinIME);
|
||||
}
|
||||
|
||||
// TODO: currently unreachable. Remove after being sure no menu is needed.
|
||||
/*
|
||||
public void presentResearchDialog(final LatinIME latinIME) {
|
||||
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;
|
||||
private void cancelRecording() {
|
||||
if (mUserRecordingLog != null) {
|
||||
mUserRecordingLog.abort();
|
||||
}
|
||||
mUserRecordingLog = null;
|
||||
mUserRecordingLogBuffer = null;
|
||||
}
|
||||
|
||||
};
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(latinIME)
|
||||
.setItems(items, listener)
|
||||
.setTitle(title);
|
||||
latinIME.showOptionDialog(builder.create());
|
||||
private void startRecording() {
|
||||
// Don't record the "start recording" motion.
|
||||
commitCurrentLogUnit();
|
||||
if (mUserRecordingLog != null) {
|
||||
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;
|
||||
|
||||
|
@ -631,38 +649,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
|||
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 =
|
||||
new LogStatement("UserFeedback", false, false, "contents", "accountName");
|
||||
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,
|
||||
final Object... values) {
|
||||
assert values.length == logStatement.mKeys.length;
|
||||
assert values.length == logStatement.getKeys().length;
|
||||
if (isAllowedToLog() && logUnit != null) {
|
||||
final long time = SystemClock.uptimeMillis();
|
||||
logUnit.addLogStatement(logStatement, time, values);
|
||||
|
@ -801,6 +787,9 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
|||
if (mFeedbackLogBuffer != null) {
|
||||
mFeedbackLogBuffer.shiftIn(mCurrentLogUnit);
|
||||
}
|
||||
if (mUserRecordingLogBuffer != null) {
|
||||
mUserRecordingLogBuffer.shiftIn(mCurrentLogUnit);
|
||||
}
|
||||
mCurrentLogUnit = new LogUnit();
|
||||
} else {
|
||||
if (DEBUG) {
|
||||
|
@ -1058,7 +1047,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
|||
*
|
||||
*/
|
||||
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,
|
||||
final long eventTime, final int index, final int id, final int x, final int y) {
|
||||
if (me != null) {
|
||||
|
@ -1075,7 +1064,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
|||
}
|
||||
final ResearchLogger researchLogger = getInstance();
|
||||
researchLogger.enqueueEvent(LOGSTATEMENT_MAIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENT,
|
||||
actionString, MotionEvent.obtain(me));
|
||||
actionString, MotionEvent.obtain(me), false);
|
||||
if (action == MotionEvent.ACTION_DOWN) {
|
||||
// Subtract 1 from eventTime so the down event is included in the later
|
||||
// LogUnit, not the earlier (the test is for inequality).
|
||||
|
@ -1442,12 +1431,20 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
|||
final int code) {
|
||||
if (key != null) {
|
||||
String outputText = key.getOutputText();
|
||||
getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCODEINPUT,
|
||||
final ResearchLogger researchLogger = getInstance();
|
||||
researchLogger.enqueueEvent(LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCODEINPUT,
|
||||
Constants.printableCode(scrubDigitFromCodePoint(code)),
|
||||
outputText == null ? null : scrubDigitsFromString(outputText.toString()),
|
||||
x, y, ignoreModifierKey, altersCode, key.isEnabled());
|
||||
if (code == Constants.CODE_RESEARCH) {
|
||||
researchLogger.suppressResearchKeyMotionData();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void suppressResearchKeyMotionData() {
|
||||
mCurrentLogUnit.removeResearchButtonInvocation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a call to PointerTracker.callListenerCallListenerOnRelease().
|
||||
|
|
|
@ -131,7 +131,7 @@ public final class UploaderService extends IntentService {
|
|||
final File[] files = mFilesDir.listFiles(new FileFilter() {
|
||||
@Override
|
||||
public boolean accept(File pathname) {
|
||||
return pathname.getName().startsWith(ResearchLogger.FILENAME_PREFIX)
|
||||
return pathname.getName().startsWith(ResearchLogger.LOG_FILENAME_PREFIX)
|
||||
&& !pathname.canWrite();
|
||||
}
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue