aboutsummaryrefslogtreecommitdiffstats
path: root/java/src/com/android/inputmethod/research
diff options
context:
space:
mode:
Diffstat (limited to 'java/src/com/android/inputmethod/research')
-rw-r--r--java/src/com/android/inputmethod/research/BootBroadcastReceiver.java16
-rw-r--r--java/src/com/android/inputmethod/research/FeedbackActivity.java31
-rw-r--r--java/src/com/android/inputmethod/research/FeedbackFragment.java152
-rw-r--r--java/src/com/android/inputmethod/research/FeedbackLayout.java16
-rw-r--r--java/src/com/android/inputmethod/research/FixedLogBuffer.java162
-rw-r--r--java/src/com/android/inputmethod/research/JsonUtils.java157
-rw-r--r--java/src/com/android/inputmethod/research/LogBuffer.java120
-rw-r--r--java/src/com/android/inputmethod/research/LogStatement.java224
-rw-r--r--java/src/com/android/inputmethod/research/LogUnit.java456
-rw-r--r--java/src/com/android/inputmethod/research/LoggingUtils.java38
-rw-r--r--java/src/com/android/inputmethod/research/MainLogBuffer.java219
-rw-r--r--java/src/com/android/inputmethod/research/MotionEventReader.java332
-rw-r--r--java/src/com/android/inputmethod/research/Replayer.java149
-rw-r--r--java/src/com/android/inputmethod/research/ReplayerService.java65
-rw-r--r--java/src/com/android/inputmethod/research/ResearchLog.java178
-rw-r--r--java/src/com/android/inputmethod/research/ResearchLogger.java1761
-rw-r--r--java/src/com/android/inputmethod/research/Statistics.java140
-rw-r--r--java/src/com/android/inputmethod/research/UploaderService.java39
18 files changed, 3319 insertions, 936 deletions
diff --git a/java/src/com/android/inputmethod/research/BootBroadcastReceiver.java b/java/src/com/android/inputmethod/research/BootBroadcastReceiver.java
index 5124a35a6..c5f095919 100644
--- a/java/src/com/android/inputmethod/research/BootBroadcastReceiver.java
+++ b/java/src/com/android/inputmethod/research/BootBroadcastReceiver.java
@@ -1,17 +1,17 @@
/*
* 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
+ * 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
+ * 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.
+ * 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;
diff --git a/java/src/com/android/inputmethod/research/FeedbackActivity.java b/java/src/com/android/inputmethod/research/FeedbackActivity.java
index 11eae8813..b985fda21 100644
--- a/java/src/com/android/inputmethod/research/FeedbackActivity.java
+++ b/java/src/com/android/inputmethod/research/FeedbackActivity.java
@@ -1,17 +1,17 @@
/*
* 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
+ * 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
+ * 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.
+ * 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;
@@ -28,25 +28,10 @@ public class FeedbackActivity extends Activity {
super.onCreate(savedInstanceState);
setContentView(R.layout.research_feedback_activity);
final FeedbackLayout layout = (FeedbackLayout) findViewById(R.id.research_feedback_layout);
- final CheckBox checkbox = (CheckBox) findViewById(R.id.research_feedback_include_history);
- final CharSequence cs = checkbox.getText();
- final String actualString = String.format(cs.toString(),
- ResearchLogger.FEEDBACK_WORD_BUFFER_SIZE);
- checkbox.setText(actualString);
layout.setActivity(this);
}
@Override
- protected void onResume() {
- super.onResume();
- }
-
- @Override
- protected void onPause() {
- super.onPause();
- }
-
- @Override
public void onBackPressed() {
ResearchLogger.getInstance().onLeavingSendFeedbackDialog();
super.onBackPressed();
diff --git a/java/src/com/android/inputmethod/research/FeedbackFragment.java b/java/src/com/android/inputmethod/research/FeedbackFragment.java
index a2e08e2b7..39f9c87a0 100644
--- a/java/src/com/android/inputmethod/research/FeedbackFragment.java
+++ b/java/src/com/android/inputmethod/research/FeedbackFragment.java
@@ -1,17 +1,17 @@
/*
* 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
+ * 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
+ * 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.
+ * 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;
@@ -20,6 +20,8 @@ import android.app.Activity;
import android.app.Fragment;
import android.os.Bundle;
import android.text.Editable;
+import android.text.TextUtils;
+import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
@@ -27,12 +29,22 @@ import android.view.ViewGroup;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
+import android.widget.Toast;
import com.android.inputmethod.latin.R;
-public class FeedbackFragment extends Fragment {
+public class FeedbackFragment extends Fragment implements OnClickListener {
+ private static final String TAG = FeedbackFragment.class.getSimpleName();
+
+ public static final String KEY_FEEDBACK_STRING = "FeedbackString";
+ public static final String KEY_INCLUDE_ACCOUNT_NAME = "IncludeAccountName";
+ public static final String KEY_HAS_USER_RECORDING = "HasRecording";
+
private EditText mEditText;
- private CheckBox mCheckBox;
+ private CheckBox mIncludingAccountNameCheckBox;
+ private CheckBox mIncludingUserRecordingCheckBox;
+ private Button mSendButton;
+ private Button mCancelButton;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
@@ -40,34 +52,102 @@ public class FeedbackFragment extends Fragment {
final View view = inflater.inflate(R.layout.research_feedback_fragment_layout, container,
false);
mEditText = (EditText) view.findViewById(R.id.research_feedback_contents);
- mCheckBox = (CheckBox) view.findViewById(R.id.research_feedback_include_history);
-
- final Button sendButton = (Button) view.findViewById(
- R.id.research_feedback_send_button);
- sendButton.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- final Editable editable = mEditText.getText();
- final String feedbackContents = editable.toString();
- final boolean includeHistory = mCheckBox.isChecked();
- ResearchLogger.getInstance().sendFeedback(feedbackContents, includeHistory);
- final Activity activity = FeedbackFragment.this.getActivity();
- activity.finish();
- ResearchLogger.getInstance().onLeavingSendFeedbackDialog();
+ mEditText.requestFocus();
+ mIncludingAccountNameCheckBox = (CheckBox) view.findViewById(
+ R.id.research_feedback_include_account_name);
+ mIncludingUserRecordingCheckBox = (CheckBox) view.findViewById(
+ R.id.research_feedback_include_recording_checkbox);
+ mIncludingUserRecordingCheckBox.setOnClickListener(this);
+
+ mSendButton = (Button) view.findViewById(R.id.research_feedback_send_button);
+ mSendButton.setOnClickListener(this);
+ mCancelButton = (Button) view.findViewById(R.id.research_feedback_cancel_button);
+ mCancelButton.setOnClickListener(this);
+
+ if (savedInstanceState != null) {
+ Log.d(TAG, "restoring from savedInstanceState");
+ restoreState(savedInstanceState);
+ } else {
+ final Bundle bundle = getActivity().getIntent().getExtras();
+ if (bundle != null) {
+ Log.d(TAG, "restoring from getArguments()");
+ restoreState(bundle);
}
- });
-
- final Button cancelButton = (Button) view.findViewById(
- R.id.research_feedback_cancel_button);
- cancelButton.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- final Activity activity = FeedbackFragment.this.getActivity();
- activity.finish();
- ResearchLogger.getInstance().onLeavingSendFeedbackDialog();
+ }
+ return view;
+ }
+
+ @Override
+ public void onClick(final View view) {
+ final ResearchLogger researchLogger = ResearchLogger.getInstance();
+ if (view == mIncludingUserRecordingCheckBox) {
+ if (hasUserRecording()) {
+ // Remove the recording
+ setHasUserRecording(false);
+ } else {
+ final Bundle bundle = new Bundle();
+ onSaveInstanceState(bundle);
+
+ // Let the user make a recording
+ getActivity().finish();
+
+ researchLogger.setFeedbackDialogBundle(bundle);
+ researchLogger.onLeavingSendFeedbackDialog();
+ researchLogger.startRecording();
}
- });
+ } else if (view == mSendButton) {
+ final Editable editable = mEditText.getText();
+ final String feedbackContents = editable.toString();
+ if (TextUtils.isEmpty(feedbackContents)) {
+ Toast.makeText(getActivity(),
+ R.string.research_feedback_empty_feedback_error_message,
+ Toast.LENGTH_LONG).show();
+ } else {
+ final boolean isIncludingAccountName = isIncludingAccountName();
+ researchLogger.sendFeedback(feedbackContents,
+ false /* isIncludingHistory */, isIncludingAccountName, hasUserRecording());
+ getActivity().finish();
+ researchLogger.setFeedbackDialogBundle(null);
+ researchLogger.onLeavingSendFeedbackDialog();
+ }
+ } else if (view == mCancelButton) {
+ Log.d(TAG, "Finishing");
+ getActivity().finish();
+ researchLogger.setFeedbackDialogBundle(null);
+ researchLogger.onLeavingSendFeedbackDialog();
+ } else {
+ Log.e(TAG, "Unknown view passed to FeedbackFragment.onClick()");
+ }
+ }
- return view;
+ @Override
+ public void onSaveInstanceState(final Bundle bundle) {
+ final String savedFeedbackString = mEditText.getText().toString();
+
+ bundle.putString(KEY_FEEDBACK_STRING, savedFeedbackString);
+ bundle.putBoolean(KEY_INCLUDE_ACCOUNT_NAME, isIncludingAccountName());
+ bundle.putBoolean(KEY_HAS_USER_RECORDING, hasUserRecording());
+ }
+
+ public void restoreState(final Bundle bundle) {
+ mEditText.setText(bundle.getString(KEY_FEEDBACK_STRING));
+ setIsIncludingAccountName(bundle.getBoolean(KEY_INCLUDE_ACCOUNT_NAME));
+ setHasUserRecording(bundle.getBoolean(KEY_HAS_USER_RECORDING));
+ }
+
+ private boolean hasUserRecording() {
+ return mIncludingUserRecordingCheckBox.isChecked();
+ }
+
+ private void setHasUserRecording(final boolean hasRecording) {
+ mIncludingUserRecordingCheckBox.setChecked(hasRecording);
+ }
+
+ private boolean isIncludingAccountName() {
+ return mIncludingAccountNameCheckBox.isChecked();
+ }
+
+ private void setIsIncludingAccountName(final boolean isIncludingAccountName) {
+ mIncludingAccountNameCheckBox.setChecked(isIncludingAccountName);
}
}
diff --git a/java/src/com/android/inputmethod/research/FeedbackLayout.java b/java/src/com/android/inputmethod/research/FeedbackLayout.java
index f2cbfe308..d283d14b2 100644
--- a/java/src/com/android/inputmethod/research/FeedbackLayout.java
+++ b/java/src/com/android/inputmethod/research/FeedbackLayout.java
@@ -1,17 +1,17 @@
/*
* 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
+ * 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
+ * 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.
+ * 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;
diff --git a/java/src/com/android/inputmethod/research/FixedLogBuffer.java b/java/src/com/android/inputmethod/research/FixedLogBuffer.java
new file mode 100644
index 000000000..78dc59562
--- /dev/null
+++ b/java/src/com/android/inputmethod/research/FixedLogBuffer.java
@@ -0,0 +1,162 @@
+/*
+ * 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;
+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.
+ *
+ * This variant of a LogBuffer has a limited memory footprint because of its limited size. This
+ * makes it useful, for example, for recording a window of the user's most recent actions in case
+ * they want to report an observed error that they do not know how to reproduce.
+ */
+public class FixedLogBuffer extends LogBuffer {
+ /* package for test */ int mWordCapacity;
+ // The number of members of mLogUnits that are actual words.
+ private 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
+ */
+ public FixedLogBuffer(final int wordCapacity) {
+ super();
+ if (wordCapacity <= 0) {
+ throw new IllegalArgumentException("wordCapacity must be 1 or greater.");
+ }
+ mWordCapacity = wordCapacity;
+ mNumActualWords = 0;
+ }
+
+ protected int getNumActualWords() {
+ return mNumActualWords;
+ }
+
+ /**
+ * Adds a new LogUnit to the front of the LIFO queue, evicting existing LogUnit's
+ * (oldest first) if word capacity is reached.
+ */
+ @Override
+ public void shiftIn(final LogUnit newLogUnit) {
+ if (!newLogUnit.hasWord()) {
+ // This LogUnit isn't a word, so it doesn't count toward the word-limit.
+ super.shiftIn(newLogUnit);
+ return;
+ }
+ if (mNumActualWords >= mWordCapacity) {
+ // Give subclass a chance to handle the buffer full condition by shifting out logUnits.
+ onBufferFull();
+ // If still full, evict.
+ if (mNumActualWords >= mWordCapacity) {
+ shiftOutWords(1);
+ }
+ }
+ super.shiftIn(newLogUnit);
+ mNumActualWords++; // Must be a word, or we wouldn't be here.
+ }
+
+ @Override
+ public LogUnit unshiftIn() {
+ final LogUnit logUnit = super.unshiftIn();
+ if (logUnit != null && logUnit.hasWord()) {
+ mNumActualWords--;
+ }
+ return logUnit;
+ }
+
+ public int getNumWords() {
+ return mNumActualWords;
+ }
+
+ /**
+ * Removes all LogUnits from the buffer without calling onShiftOut().
+ */
+ @Override
+ public void clear() {
+ super.clear();
+ mNumActualWords = 0;
+ }
+
+ /**
+ * Called when the buffer has just shifted in one more word than its maximum, and its about to
+ * shift out LogUnits to bring it back down to the maximum.
+ *
+ * Base class does nothing; subclasses may override if they want to record non-privacy sensitive
+ * events that fall off the end.
+ */
+ protected void onBufferFull() {
+ }
+
+ @Override
+ public LogUnit shiftOut() {
+ final LogUnit logUnit = super.shiftOut();
+ if (logUnit != null && logUnit.hasWord()) {
+ mNumActualWords--;
+ }
+ return logUnit;
+ }
+
+ protected void shiftOutWords(final int numWords) {
+ final int targetNumWords = mNumActualWords - numWords;
+ final LinkedList<LogUnit> logUnits = getLogUnits();
+ while (mNumActualWords > targetNumWords && !logUnits.isEmpty()) {
+ shiftOut();
+ }
+ }
+
+ public void shiftOutAll() {
+ final LinkedList<LogUnit> logUnits = getLogUnits();
+ while (!logUnits.isEmpty()) {
+ shiftOut();
+ }
+ mNumActualWords = 0;
+ }
+
+ /**
+ * Returns a list of {@link LogUnit}s at the front of the buffer that have associated words. No
+ * more than {@code n} LogUnits will have words associated with them. If there are not enough
+ * LogUnits in the buffer to meet the word requirement, returns the all LogUnits.
+ *
+ * @param n The maximum number of {@link LogUnit}s with words to return.
+ * @return The list of the {@link LogUnit}s containing the first n words
+ */
+ public ArrayList<LogUnit> peekAtFirstNWords(int n) {
+ final LinkedList<LogUnit> logUnits = getLogUnits();
+ final int length = logUnits.size();
+ // Allocate space for n*2 logUnits. There will be at least n, one for each word, and
+ // there may be additional for punctuation, between-word commands, etc. This should be
+ // enough that reallocation won't be necessary.
+ final ArrayList<LogUnit> list = new ArrayList<LogUnit>(n * 2);
+ for (int i = 0; i < length && n > 0; i++) {
+ final LogUnit logUnit = logUnits.get(i);
+ list.add(logUnit);
+ if (logUnit.hasWord()) {
+ n--;
+ }
+ }
+ return list;
+ }
+}
diff --git a/java/src/com/android/inputmethod/research/JsonUtils.java b/java/src/com/android/inputmethod/research/JsonUtils.java
new file mode 100644
index 000000000..24cd8d935
--- /dev/null
+++ b/java/src/com/android/inputmethod/research/JsonUtils.java
@@ -0,0 +1,157 @@
+/*
+ * 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.content.SharedPreferences;
+import android.util.JsonWriter;
+import android.view.MotionEvent;
+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 java.io.IOException;
+import java.util.Map;
+
+/**
+ * Routines for mapping classes and variables to JSON representations for logging.
+ */
+/* package */ class JsonUtils {
+ private JsonUtils() {
+ // This utility class is not publicly instantiable.
+ }
+
+ /* package */ static void writeJson(final CompletionInfo[] ci, final JsonWriter jsonWriter)
+ throws IOException {
+ jsonWriter.beginArray();
+ for (int j = 0; j < ci.length; j++) {
+ jsonWriter.value(ci[j].toString());
+ }
+ jsonWriter.endArray();
+ }
+
+ /* package */ static void writeJson(final SharedPreferences prefs, final JsonWriter jsonWriter)
+ throws IOException {
+ jsonWriter.beginObject();
+ for (Map.Entry<String,?> entry : prefs.getAll().entrySet()) {
+ jsonWriter.name(entry.getKey());
+ final Object innerValue = entry.getValue();
+ if (innerValue == null) {
+ jsonWriter.nullValue();
+ } else if (innerValue instanceof Boolean) {
+ jsonWriter.value((Boolean) innerValue);
+ } else if (innerValue instanceof Number) {
+ jsonWriter.value((Number) innerValue);
+ } else {
+ jsonWriter.value(innerValue.toString());
+ }
+ }
+ jsonWriter.endObject();
+ }
+
+ /* package */ static void writeJson(final Key[] keys, final JsonWriter jsonWriter)
+ throws IOException {
+ jsonWriter.beginArray();
+ for (Key key : keys) {
+ writeJson(key, jsonWriter);
+ }
+ jsonWriter.endArray();
+ }
+
+ private static void writeJson(final Key key, final JsonWriter jsonWriter) throws IOException {
+ jsonWriter.beginObject();
+ jsonWriter.name("code").value(key.mCode);
+ jsonWriter.name("altCode").value(key.getAltCode());
+ jsonWriter.name("x").value(key.mX);
+ jsonWriter.name("y").value(key.mY);
+ jsonWriter.name("w").value(key.mWidth);
+ jsonWriter.name("h").value(key.mHeight);
+ jsonWriter.endObject();
+ }
+
+ /* package */ static void writeJson(final SuggestedWords words, final JsonWriter jsonWriter)
+ throws IOException {
+ jsonWriter.beginObject();
+ jsonWriter.name("typedWordValid").value(words.mTypedWordValid);
+ jsonWriter.name("willAutoCorrect")
+ .value(words.mWillAutoCorrect);
+ jsonWriter.name("isPunctuationSuggestions")
+ .value(words.mIsPunctuationSuggestions);
+ jsonWriter.name("isObsoleteSuggestions").value(words.mIsObsoleteSuggestions);
+ jsonWriter.name("isPrediction").value(words.mIsPrediction);
+ jsonWriter.name("words");
+ jsonWriter.beginArray();
+ final int size = words.size();
+ for (int j = 0; j < size; j++) {
+ final SuggestedWordInfo wordInfo = words.getInfo(j);
+ jsonWriter.value(wordInfo.toString());
+ }
+ jsonWriter.endArray();
+ jsonWriter.endObject();
+ }
+
+ /* package */ static void writeJson(final MotionEvent me, final JsonWriter jsonWriter)
+ throws IOException {
+ jsonWriter.beginObject();
+ jsonWriter.name("pointerIds");
+ jsonWriter.beginArray();
+ final int pointerCount = me.getPointerCount();
+ for (int index = 0; index < pointerCount; index++) {
+ jsonWriter.value(me.getPointerId(index));
+ }
+ jsonWriter.endArray();
+
+ jsonWriter.name("xyt");
+ jsonWriter.beginArray();
+ final int historicalSize = me.getHistorySize();
+ for (int index = 0; index < historicalSize; index++) {
+ jsonWriter.beginObject();
+ jsonWriter.name("t");
+ jsonWriter.value(me.getHistoricalEventTime(index));
+ jsonWriter.name("d");
+ jsonWriter.beginArray();
+ for (int pointerIndex = 0; pointerIndex < pointerCount; pointerIndex++) {
+ jsonWriter.beginObject();
+ jsonWriter.name("x");
+ jsonWriter.value(me.getHistoricalX(pointerIndex, index));
+ jsonWriter.name("y");
+ jsonWriter.value(me.getHistoricalY(pointerIndex, index));
+ jsonWriter.endObject();
+ }
+ jsonWriter.endArray();
+ jsonWriter.endObject();
+ }
+ jsonWriter.beginObject();
+ jsonWriter.name("t");
+ jsonWriter.value(me.getEventTime());
+ jsonWriter.name("d");
+ jsonWriter.beginArray();
+ for (int pointerIndex = 0; pointerIndex < pointerCount; pointerIndex++) {
+ jsonWriter.beginObject();
+ jsonWriter.name("x");
+ jsonWriter.value(me.getX(pointerIndex));
+ jsonWriter.name("y");
+ jsonWriter.value(me.getY(pointerIndex));
+ jsonWriter.endObject();
+ }
+ jsonWriter.endArray();
+ jsonWriter.endObject();
+ jsonWriter.endArray();
+ jsonWriter.endObject();
+ }
+}
diff --git a/java/src/com/android/inputmethod/research/LogBuffer.java b/java/src/com/android/inputmethod/research/LogBuffer.java
index ae7b1579a..b07b761f0 100644
--- a/java/src/com/android/inputmethod/research/LogBuffer.java
+++ b/java/src/com/android/inputmethod/research/LogBuffer.java
@@ -1,113 +1,73 @@
/*
* 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
+ * 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
+ * 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.
+ * 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.CollectionUtils;
-
import java.util.LinkedList;
/**
- * A buffer that holds a fixed number of LogUnits.
+ * Maintain a FIFO queue 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.
+ * This class provides an unbounded queue. This is useful when the user is aware that their actions
+ * are being recorded, such as when they are trying to reproduce a bug. In this case, there should
+ * not be artificial restrictions on how many events that can be saved.
*/
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;
+ // TODO: Gracefully handle situations in which this LogBuffer is consuming too much memory.
+ // This may happen, for example, if the user has forgotten that data is being logged.
+ private final LinkedList<LogUnit> mLogUnits;
- /**
- * 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 = CollectionUtils.newLinkedList();
- 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.
+ public LogBuffer() {
+ mLogUnits = new LinkedList<LogUnit>();
}
- 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;
- }
- }
+ protected LinkedList<LogUnit> getLogUnits() {
+ return mLogUnits;
}
- /**
- * 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) {
+ public void shiftIn(final LogUnit logUnit) {
+ mLogUnits.add(logUnit);
}
- /**
- * Called to deliberately remove the oldest LogUnit. Usually called when draining the
- * LogBuffer.
- */
- public LogUnit shiftOut() {
+ public LogUnit unshiftIn() {
if (mLogUnits.isEmpty()) {
return null;
}
- final LogUnit logUnit = mLogUnits.removeFirst();
- if (logUnit.hasWord()) {
- mNumActualWords--;
+ return mLogUnits.removeLast();
+ }
+
+ public LogUnit peekLastLogUnit() {
+ if (mLogUnits.isEmpty()) {
+ return null;
+ }
+ return mLogUnits.peekLast();
+ }
+
+ public boolean isEmpty() {
+ return mLogUnits.isEmpty();
+ }
+
+ public LogUnit shiftOut() {
+ if (isEmpty()) {
+ return null;
}
- return logUnit;
+ return mLogUnits.removeFirst();
}
}
diff --git a/java/src/com/android/inputmethod/research/LogStatement.java b/java/src/com/android/inputmethod/research/LogStatement.java
new file mode 100644
index 000000000..09b12fcfa
--- /dev/null
+++ b/java/src/com/android/inputmethod/research/LogStatement.java
@@ -0,0 +1,224 @@
+/*
+ * 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.content.SharedPreferences;
+import android.util.JsonWriter;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.inputmethod.CompletionInfo;
+
+import com.android.inputmethod.keyboard.Key;
+import com.android.inputmethod.latin.SuggestedWords;
+import com.android.inputmethod.latin.define.ProductionFlag;
+
+import java.io.IOException;
+
+/**
+ * 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.
+ */
+public class LogStatement {
+ private static final String TAG = LogStatement.class.getSimpleName();
+ private static final boolean DEBUG = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG;
+
+ // 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_MAIN_KEYBOARD_VIEW_ON_LONG_PRESS =
+ "MainKeyboardViewOnLongPress";
+ public static final String ACTION = "action";
+ public static final String VALUE_DOWN = "DOWN";
+ public static final String TYPE_MOTION_EVENT = "MotionEvent";
+ public static final String KEY_IS_LOGGING_RELATED = "isLoggingRelated";
+
+ // Keys for internal key/value pairs
+ private static final String CURRENT_TIME_KEY = "_ct";
+ private static final String UPTIME_KEY = "_ut";
+ private static final String EVENT_TYPE_KEY = "_ty";
+
+ // 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;
+ }
+
+ /**
+ * Write the contents out through jsonWriter.
+ *
+ * The JsonWriter class must have already had {@code JsonWriter.beginArray} called on it.
+ *
+ * Note that this method is not thread safe for the same jsonWriter. Callers must ensure
+ * thread safety.
+ */
+ public boolean outputToLocked(final JsonWriter jsonWriter, final Long time,
+ final Object... values) {
+ if (DEBUG) {
+ if (mKeys.length != values.length) {
+ Log.d(TAG, "Key and Value list sizes do not match. " + mType);
+ }
+ }
+ try {
+ jsonWriter.beginObject();
+ jsonWriter.name(CURRENT_TIME_KEY).value(System.currentTimeMillis());
+ jsonWriter.name(UPTIME_KEY).value(time);
+ jsonWriter.name(EVENT_TYPE_KEY).value(mType);
+ final int length = values.length;
+ for (int i = 0; i < length; i++) {
+ jsonWriter.name(mKeys[i]);
+ final Object value = values[i];
+ if (value instanceof CharSequence) {
+ jsonWriter.value(value.toString());
+ } else if (value instanceof Number) {
+ jsonWriter.value((Number) value);
+ } else if (value instanceof Boolean) {
+ jsonWriter.value((Boolean) value);
+ } else if (value instanceof CompletionInfo[]) {
+ JsonUtils.writeJson((CompletionInfo[]) value, jsonWriter);
+ } else if (value instanceof SharedPreferences) {
+ JsonUtils.writeJson((SharedPreferences) value, jsonWriter);
+ } else if (value instanceof Key[]) {
+ JsonUtils.writeJson((Key[]) value, jsonWriter);
+ } else if (value instanceof SuggestedWords) {
+ JsonUtils.writeJson((SuggestedWords) value, jsonWriter);
+ } else if (value instanceof MotionEvent) {
+ JsonUtils.writeJson((MotionEvent) value, jsonWriter);
+ } else if (value == null) {
+ jsonWriter.nullValue();
+ } else {
+ if (DEBUG) {
+ Log.w(TAG, "Unrecognized type to be logged: "
+ + (value == null ? "<null>" : value.getClass().getName()));
+ }
+ jsonWriter.nullValue();
+ }
+ }
+ jsonWriter.endObject();
+ } catch (IOException e) {
+ e.printStackTrace();
+ Log.w(TAG, "Error in JsonWriter; skipping LogStatement");
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/java/src/com/android/inputmethod/research/LogUnit.java b/java/src/com/android/inputmethod/research/LogUnit.java
index d8b3a29ff..e91976a03 100644
--- a/java/src/com/android/inputmethod/research/LogUnit.java
+++ b/java/src/com/android/inputmethod/research/LogUnit.java
@@ -1,24 +1,35 @@
/*
* 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
+ * 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
+ * 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.
+ * 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.CollectionUtils;
+import android.content.SharedPreferences;
+import android.os.SystemClock;
+import android.text.TextUtils;
+import android.util.JsonWriter;
+import android.util.Log;
+import com.android.inputmethod.latin.SuggestedWords;
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+import com.android.inputmethod.latin.define.ProductionFlag;
+
+import java.io.IOException;
+import java.io.StringWriter;
import java.util.ArrayList;
+import java.util.List;
/**
* A group of log statements related to each other.
@@ -35,29 +46,200 @@ import java.util.ArrayList;
* been published recently, or whether the LogUnit contains numbers, etc.
*/
/* package */ class LogUnit {
- private final ArrayList<String[]> mKeysList = CollectionUtils.newArrayList();
- private final ArrayList<Object[]> mValuesList = CollectionUtils.newArrayList();
- private final ArrayList<Boolean> mIsPotentiallyPrivate = CollectionUtils.newArrayList();
+ 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
+ // mTimeList.
+ private final ArrayList<Long> mTimeList;
+ // Word that this LogUnit generates. Should be null if the LogUnit does not generate a genuine
+ // word (i.e. separators alone do not count as a word). Should never be empty.
private String mWord;
- private boolean mContainsDigit;
+ private boolean mMayContainDigit;
+ private boolean mIsPartOfMegaword;
+ private boolean mContainsCorrection;
+
+ // mCorrectionType indicates whether the word was corrected at all, and if so, whether it was
+ // to a different word or just a "typo" correction. It is considered a "typo" if the final
+ // word was listed in the suggestions available the first time the word was gestured or
+ // tapped.
+ private int mCorrectionType;
+ public static final int CORRECTIONTYPE_NO_CORRECTION = 0;
+ public static final int CORRECTIONTYPE_CORRECTION = 1;
+ public static final int CORRECTIONTYPE_DIFFERENT_WORD = 2;
+ public static final int CORRECTIONTYPE_TYPO = 3;
- public void addLogStatement(final String[] keys, final Object[] values,
- final Boolean isPotentiallyPrivate) {
- mKeysList.add(keys);
+ private SuggestedWords mSuggestedWords;
+
+ public LogUnit() {
+ mLogStatementList = new ArrayList<LogStatement>();
+ mValuesList = new ArrayList<Object[]>();
+ mTimeList = new ArrayList<Long>();
+ mIsPartOfMegaword = false;
+ mCorrectionType = CORRECTIONTYPE_NO_CORRECTION;
+ mSuggestedWords = null;
+ }
+
+ private LogUnit(final ArrayList<LogStatement> logStatementList,
+ final ArrayList<Object[]> valuesList,
+ final ArrayList<Long> timeList,
+ final boolean isPartOfMegaword) {
+ mLogStatementList = logStatementList;
+ mValuesList = valuesList;
+ mTimeList = timeList;
+ mIsPartOfMegaword = isPartOfMegaword;
+ mCorrectionType = CORRECTIONTYPE_NO_CORRECTION;
+ mSuggestedWords = null;
+ }
+
+ private static final Object[] NULL_VALUES = new Object[0];
+ /**
+ * Adds a new log statement. The time parameter in successive calls to this method must be
+ * monotonically increasing, or splitByTime() will not work.
+ */
+ public void addLogStatement(final LogStatement logStatement, final long time,
+ Object... values) {
+ if (values == null) {
+ values = NULL_VALUES;
+ }
+ mLogStatementList.add(logStatement);
mValuesList.add(values);
- mIsPotentiallyPrivate.add(isPotentiallyPrivate);
+ mTimeList.add(time);
}
- 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));
+ /**
+ * Publish the contents of this LogUnit to {@code researchLog}.
+ *
+ * For each publishable {@code LogStatement}, invoke {@link LogStatement#outputToLocked}.
+ *
+ * @param researchLog where to publish the contents of this {@code LogUnit}
+ * @param canIncludePrivateData whether the private data in this {@code LogUnit} should be
+ * included
+ */
+ public synchronized void publishTo(final ResearchLog researchLog,
+ final boolean canIncludePrivateData) {
+ // Prepare debugging output if necessary
+ final StringWriter debugStringWriter;
+ final JsonWriter debugJsonWriter;
+ if (DEBUG) {
+ debugStringWriter = new StringWriter();
+ debugJsonWriter = new JsonWriter(debugStringWriter);
+ debugJsonWriter.setIndent(" ");
+ try {
+ debugJsonWriter.beginArray();
+ } catch (IOException e) {
+ Log.e(TAG, "Could not open array in JsonWriter", e);
+ }
+ } else {
+ debugStringWriter = null;
+ debugJsonWriter = null;
+ }
+ // Write out any logStatement that passes the privacy filter.
+ final int size = mLogStatementList.size();
+ if (size != 0) {
+ // Note that jsonWriter is only set to a non-null value if the logUnit start text is
+ // output and at least one logStatement is output.
+ JsonWriter jsonWriter = null;
+ for (int i = 0; i < size; i++) {
+ final LogStatement logStatement = mLogStatementList.get(i);
+ if (!canIncludePrivateData && logStatement.isPotentiallyPrivate()) {
+ continue;
+ }
+ if (mIsPartOfMegaword && logStatement.isPotentiallyRevealing()) {
+ continue;
+ }
+ // Only retrieve the jsonWriter if we need to. If we don't get this far, then
+ // researchLog.getInitializedJsonWriterLocked() will not ever be called, and the
+ // file will not have been opened for writing.
+ if (jsonWriter == null) {
+ jsonWriter = researchLog.getInitializedJsonWriterLocked();
+ outputLogUnitStart(jsonWriter, canIncludePrivateData);
+ }
+ logStatement.outputToLocked(jsonWriter, mTimeList.get(i), mValuesList.get(i));
+ if (DEBUG) {
+ logStatement.outputToLocked(debugJsonWriter, mTimeList.get(i),
+ mValuesList.get(i));
+ }
}
+ if (jsonWriter != null) {
+ // We must have called logUnitStart earlier, so emit a logUnitStop.
+ outputLogUnitStop(jsonWriter);
+ }
+ }
+ if (DEBUG) {
+ try {
+ debugJsonWriter.endArray();
+ debugJsonWriter.flush();
+ } catch (IOException e) {
+ Log.e(TAG, "Could not close array in JsonWriter", e);
+ }
+ final String bigString = debugStringWriter.getBuffer().toString();
+ final String[] lines = bigString.split("\n");
+ for (String line : lines) {
+ Log.d(TAG, line);
+ }
+ }
+ }
+
+ private static final String WORD_KEY = "_wo";
+ private static final String CORRECTION_TYPE_KEY = "_corType";
+ private static final String LOG_UNIT_BEGIN_KEY = "logUnitStart";
+ private static final String LOG_UNIT_END_KEY = "logUnitEnd";
+
+ final LogStatement LOGSTATEMENT_LOG_UNIT_BEGIN_WITH_PRIVATE_DATA =
+ new LogStatement(LOG_UNIT_BEGIN_KEY, false /* isPotentiallyPrivate */,
+ false /* isPotentiallyRevealing */, WORD_KEY, CORRECTION_TYPE_KEY);
+ final LogStatement LOGSTATEMENT_LOG_UNIT_BEGIN_WITHOUT_PRIVATE_DATA =
+ new LogStatement(LOG_UNIT_BEGIN_KEY, false /* isPotentiallyPrivate */,
+ false /* isPotentiallyRevealing */);
+ private void outputLogUnitStart(final JsonWriter jsonWriter,
+ final boolean canIncludePrivateData) {
+ final LogStatement logStatement;
+ if (canIncludePrivateData) {
+ LOGSTATEMENT_LOG_UNIT_BEGIN_WITH_PRIVATE_DATA.outputToLocked(jsonWriter,
+ SystemClock.uptimeMillis(), getWord(), getCorrectionType());
+ } else {
+ LOGSTATEMENT_LOG_UNIT_BEGIN_WITHOUT_PRIVATE_DATA.outputToLocked(jsonWriter,
+ SystemClock.uptimeMillis());
}
}
- public void setWord(String word) {
+ final LogStatement LOGSTATEMENT_LOG_UNIT_END =
+ new LogStatement(LOG_UNIT_END_KEY, false /* isPotentiallyPrivate */,
+ false /* isPotentiallyRevealing */);
+ private void outputLogUnitStop(final JsonWriter jsonWriter) {
+ LOGSTATEMENT_LOG_UNIT_END.outputToLocked(jsonWriter, SystemClock.uptimeMillis());
+ }
+
+ /**
+ * Mark the current logUnit as containing data to generate {@code word}.
+ *
+ * If {@code setWord()} was previously called for this LogUnit, then the method will try to
+ * determine what kind of correction it is, and update its internal state of the correctionType
+ * accordingly.
+ *
+ * @param word The word this LogUnit generates. Caller should not pass null or the empty
+ * string.
+ */
+ public void setWord(final String word) {
+ if (hasWord()) {
+ // The word was already set once, and it is now being changed. See if the new word
+ // is close to the old word. If so, then the change is probably a typo correction.
+ // If not, the user may have decided to enter a different word, so flag it.
+ if (mSuggestedWords != null) {
+ if (isInSuggestedWords(word, mSuggestedWords)) {
+ mCorrectionType = CORRECTIONTYPE_TYPO;
+ } else {
+ mCorrectionType = CORRECTIONTYPE_DIFFERENT_WORD;
+ }
+ } else {
+ // No suggested words, so it's not clear whether it's a typo or different word.
+ // Mark it as a generic correction.
+ mCorrectionType = CORRECTIONTYPE_CORRECTION;
+ }
+ }
mWord = word;
}
@@ -66,18 +248,234 @@ import java.util.ArrayList;
}
public boolean hasWord() {
- return mWord != null;
+ return mWord != null && !TextUtils.isEmpty(mWord.trim());
+ }
+
+ public void setMayContainDigit() {
+ mMayContainDigit = true;
+ }
+
+ public boolean mayContainDigit() {
+ return mMayContainDigit;
+ }
+
+ public void setContainsCorrection() {
+ mContainsCorrection = true;
+ }
+
+ public boolean containsCorrection() {
+ return mContainsCorrection;
}
- public void setContainsDigit() {
- mContainsDigit = true;
+ public void setCorrectionType(final int correctionType) {
+ mCorrectionType = correctionType;
}
- public boolean hasDigit() {
- return mContainsDigit;
+ public int getCorrectionType() {
+ return mCorrectionType;
}
public boolean isEmpty() {
- return mKeysList.isEmpty();
+ return mLogStatementList.isEmpty();
+ }
+
+ /**
+ * Split this logUnit, with all events before maxTime staying in the current logUnit, and all
+ * events after maxTime going into a new LogUnit that is returned.
+ */
+ public LogUnit splitByTime(final long maxTime) {
+ // Assume that mTimeList is in sorted order.
+ final int length = mTimeList.size();
+ // TODO: find time by binary search, e.g. using Collections#binarySearch()
+ for (int index = 0; index < length; index++) {
+ if (mTimeList.get(index) > maxTime) {
+ final List<LogStatement> laterLogStatements =
+ mLogStatementList.subList(index, length);
+ final List<Object[]> laterValues = mValuesList.subList(index, length);
+ final List<Long> laterTimes = mTimeList.subList(index, length);
+
+ // Create the LogUnit containing the later logStatements and associated data.
+ final LogUnit newLogUnit = new LogUnit(
+ new ArrayList<LogStatement>(laterLogStatements),
+ new ArrayList<Object[]>(laterValues),
+ new ArrayList<Long>(laterTimes),
+ true /* isPartOfMegaword */);
+ newLogUnit.mWord = null;
+ newLogUnit.mMayContainDigit = mMayContainDigit;
+ newLogUnit.mContainsCorrection = mContainsCorrection;
+
+ // Purge the logStatements and associated data from this LogUnit.
+ laterLogStatements.clear();
+ laterValues.clear();
+ laterTimes.clear();
+ mIsPartOfMegaword = true;
+
+ return newLogUnit;
+ }
+ }
+ return new LogUnit();
+ }
+
+ public void append(final LogUnit logUnit) {
+ mLogStatementList.addAll(logUnit.mLogStatementList);
+ mValuesList.addAll(logUnit.mValuesList);
+ mTimeList.addAll(logUnit.mTimeList);
+ mWord = null;
+ if (logUnit.mWord != null) {
+ setWord(logUnit.mWord);
+ }
+ mMayContainDigit = mMayContainDigit || logUnit.mMayContainDigit;
+ mContainsCorrection = mContainsCorrection || logUnit.mContainsCorrection;
+ mIsPartOfMegaword = false;
+ }
+
+ public SuggestedWords getSuggestions() {
+ return mSuggestedWords;
+ }
+
+ /**
+ * Initialize the suggestions.
+ *
+ * Once set to a non-null value, the suggestions may not be changed again. This is to keep
+ * track of the list of words that are close to the user's initial effort to type the word.
+ * Only words that are close to the initial effort are considered typo corrections.
+ */
+ public void initializeSuggestions(final SuggestedWords suggestedWords) {
+ if (mSuggestedWords == null) {
+ mSuggestedWords = suggestedWords;
+ }
+ }
+
+ private static boolean isInSuggestedWords(final String queryWord,
+ final SuggestedWords suggestedWords) {
+ if (TextUtils.isEmpty(queryWord)) {
+ return false;
+ }
+ final int size = suggestedWords.size();
+ for (int i = 0; i < size; i++) {
+ final SuggestedWordInfo wordInfo = suggestedWords.getInfo(i);
+ if (queryWord.equals(wordInfo.mWord)) {
+ return true;
+ }
+ }
+ 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_MAIN_KEYBOARD_VIEW_ON_LONG_PRESS,
+ indexOfLastResearchKey);
+
+ // Look for DOWN event preceding the long press
+ final int indexOfLastDownEventBeforeLongPress =
+ findLastIndexContainingKeyValueBefore(LogStatement.TYPE_MOTION_EVENT,
+ 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_MOTION_EVENT)) {
+ logStatement.setValue(LogStatement.KEY_IS_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;
}
}
diff --git a/java/src/com/android/inputmethod/research/LoggingUtils.java b/java/src/com/android/inputmethod/research/LoggingUtils.java
new file mode 100644
index 000000000..1261d6780
--- /dev/null
+++ b/java/src/com/android/inputmethod/research/LoggingUtils.java
@@ -0,0 +1,38 @@
+/*
+ * 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.view.MotionEvent;
+
+/* package */ class LoggingUtils {
+ private LoggingUtils() {
+ // This utility class is not publicly instantiable.
+ }
+
+ /* package */ static String getMotionEventActionTypeString(final int actionType) {
+ switch (actionType) {
+ case MotionEvent.ACTION_CANCEL: return "CANCEL";
+ case MotionEvent.ACTION_UP: return "UP";
+ case MotionEvent.ACTION_DOWN: return "DOWN";
+ case MotionEvent.ACTION_POINTER_UP: return "POINTER_UP";
+ case MotionEvent.ACTION_POINTER_DOWN: return "POINTER_DOWN";
+ case MotionEvent.ACTION_MOVE: return "MOVE";
+ case MotionEvent.ACTION_OUTSIDE: return "OUTSIDE";
+ default: return "ACTION_" + actionType;
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/research/MainLogBuffer.java b/java/src/com/android/inputmethod/research/MainLogBuffer.java
index 745768d35..45b83dd76 100644
--- a/java/src/com/android/inputmethod/research/MainLogBuffer.java
+++ b/java/src/com/android/inputmethod/research/MainLogBuffer.java
@@ -1,75 +1,104 @@
/*
* 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
+ * 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
+ * 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.
+ * 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.Log;
+
import com.android.inputmethod.latin.Dictionary;
import com.android.inputmethod.latin.Suggest;
+import com.android.inputmethod.latin.define.ProductionFlag;
+import java.util.ArrayList;
+import java.util.LinkedList;
import java.util.Random;
-public class MainLogBuffer extends LogBuffer {
+/**
+ * MainLogBuffer is a FixedLogBuffer that tracks the state of LogUnits to make privacy guarantees.
+ *
+ * There are three forms of privacy protection: 1) only words in the main dictionary are allowed to
+ * be logged in enough detail to determine their contents, 2) only a subset of words are logged
+ * in detail, such as 10%, and 3) no numbers are logged.
+ *
+ * This class maintains a list of LogUnits, each corresponding to a word. As the user completes
+ * words, they are added here. But if the user backs up over their current word to edit a word
+ * entered earlier, then it is pulled out of this LogBuffer, changes are then added to the end of
+ * the LogUnit, and it is pushed back in here when the user is done. Because words may be pulled
+ * back out even after they are pushed in, we must not publish the contents of this LogBuffer too
+ * quickly. However, we cannot let the contents pile up either, or it will limit the editing that
+ * a user can perform.
+ *
+ * To balance these requirements (keep history so user can edit, flush history so it does not pile
+ * up), the LogBuffer is considered "complete" when the user has entered enough words to form an
+ * n-gram, followed by enough additional non-detailed words (that are in the 90%, as per above).
+ * Once complete, the n-gram may be published to flash storage (via the ResearchLog class).
+ * However, the additional non-detailed words are retained, in case the user backspaces to edit
+ * them. The MainLogBuffer then continues to add words, publishing individual non-detailed words
+ * as new words arrive. After enough non-detailed words have been pushed out to account for the
+ * 90% between words, the words at the front of the LogBuffer can be published as an n-gram again.
+ *
+ * If the words that would form the valid n-gram are not in the dictionary, then words are pushed
+ * through the LogBuffer one at a time until an n-gram is found that is entirely composed of
+ * dictionary words.
+ *
+ * If the user closes a session, then the entire LogBuffer is flushed, publishing any embedded
+ * n-gram containing dictionary words.
+ */
+public abstract class MainLogBuffer extends FixedLogBuffer {
+ private static final String TAG = MainLogBuffer.class.getSimpleName();
+ private static final boolean DEBUG = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG;
+
// The size of the n-grams logged. E.g. N_GRAM_SIZE = 2 means to sample bigrams.
- private static final int N_GRAM_SIZE = 2;
- // The number of words between n-grams to omit from the log.
- private static final int DEFAULT_NUMBER_OF_WORDS_BETWEEN_SAMPLES = 18;
+ public static final int N_GRAM_SIZE = 2;
- private final ResearchLog mResearchLog;
private Suggest mSuggest;
+ private boolean mIsStopping = false;
- // The minimum periodicity with which n-grams can be sampled. E.g. mWinWordPeriod is 10 if
- // every 10th bigram is sampled, i.e., words 1-8 are not, but the bigram at words 9 and 10, etc.
- // for 11-18, and the bigram at words 19 and 20. If an n-gram is not safe (e.g. it contains a
- // number in the middle or an out-of-vocabulary word), then sampling is delayed until a safe
- // n-gram does appear.
- /* package for test */ int mMinWordPeriod;
+ /* package for test */ int mNumWordsBetweenNGrams;
// Counter for words left to suppress before an n-gram can be sampled. Reset to mMinWordPeriod
// after a sample is taken.
- /* package for test */ int mWordsUntilSafeToSample;
-
- 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);
+ /* package for test */ int mNumWordsUntilSafeToSample;
+
+ public MainLogBuffer(final int wordsBetweenSamples, final int numInitialWordsToIgnore) {
+ super(N_GRAM_SIZE + wordsBetweenSamples);
+ mNumWordsBetweenNGrams = wordsBetweenSamples;
+ mNumWordsUntilSafeToSample = DEBUG ? 0 : numInitialWordsToIgnore;
}
- public void setSuggest(Suggest suggest) {
+ public void setSuggest(final Suggest suggest) {
mSuggest = suggest;
}
- @Override
- public void shiftIn(final LogUnit newLogUnit) {
- super.shiftIn(newLogUnit);
- if (newLogUnit.hasWord()) {
- if (mWordsUntilSafeToSample > 0) {
- mWordsUntilSafeToSample--;
- }
- }
+ private Dictionary getDictionary() {
+ if (mSuggest == null || !mSuggest.hasMainDictionary()) return null;
+ return mSuggest.getMainDictionary();
}
public void resetWordCounter() {
- mWordsUntilSafeToSample = mMinWordPeriod;
+ mNumWordsUntilSafeToSample = mNumWordsBetweenNGrams;
+ }
+
+ public void setIsStopping() {
+ mIsStopping = true;
}
/**
- * Determines whether the content of the MainLogBuffer can be safely uploaded in its complete
- * form and still protect the user's privacy.
+ * Determines whether uploading the n words at the front the MainLogBuffer will not violate
+ * user privacy.
*
* The size of the MainLogBuffer is just enough to hold one n-gram, its corrections, and any
* non-character data that is typed between words. The decision about privacy is made based on
@@ -78,50 +107,118 @@ public class MainLogBuffer extends LogBuffer {
* the screen orientation and other characteristics about the device can be uploaded without
* revealing much about the user.
*/
- public boolean isSafeToLog() {
+ private boolean isSafeNGram(final ArrayList<LogUnit> logUnits, final int minNGramSize) {
+ // Bypass privacy checks when debugging.
+ if (ResearchLogger.IS_LOGGING_EVERYTHING) {
+ if (mIsStopping) {
+ return true;
+ }
+ // Only check that it is the right length. If not, wait for later words to make
+ // complete n-grams.
+ int numWordsInLogUnitList = 0;
+ final int length = logUnits.size();
+ for (int i = 0; i < length; i++) {
+ final LogUnit logUnit = logUnits.get(i);
+ final String word = logUnit.getWord();
+ if (word != null) {
+ numWordsInLogUnitList++;
+ }
+ }
+ return numWordsInLogUnitList >= minNGramSize;
+ }
+
// 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.
+ if (mNumWordsUntilSafeToSample > 0) {
return false;
}
// Reload the dictionary in case it has changed (e.g., because the user has changed
// languages).
- final Dictionary dictionary = mSuggest.getMainDictionary();
+ final Dictionary dictionary = getDictionary();
if (dictionary == null) {
+ // 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;
}
- // 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();
+
+ // Check each word in the buffer. If any word poses a privacy threat, we cannot upload
+ // the complete buffer contents in detail.
+ int numWordsInLogUnitList = 0;
+ final int length = logUnits.size();
for (int i = 0; i < length; i++) {
- final LogUnit logUnit = mLogUnits.get(i);
- final String word = logUnit.getWord();
- if (word == null) {
+ final LogUnit logUnit = logUnits.get(i);
+ if (!logUnit.hasWord()) {
// Digits outside words are a privacy threat.
- if (logUnit.hasDigit()) {
+ if (logUnit.mayContainDigit()) {
return false;
}
} else {
+ numWordsInLogUnitList++;
+ final String word = logUnit.getWord();
// Words not in the dictionary are a privacy threat.
- if (!(dictionary.isValidWord(word))) {
+ if (ResearchLogger.hasLetters(word) && !(dictionary.isValidWord(word))) {
+ if (DEBUG) {
+ Log.d(TAG, "NOT SAFE!: hasLetters: " + ResearchLogger.hasLetters(word)
+ + ", isValid: " + (dictionary.isValidWord(word)));
+ }
return false;
}
}
}
- // All checks have passed; this buffer's content can be safely uploaded.
- return true;
+
+ // Finally, only return true if the minNGramSize is met.
+ return numWordsInLogUnitList >= minNGramSize;
+ }
+
+ public void shiftAndPublishAll() {
+ final LinkedList<LogUnit> logUnits = getLogUnits();
+ while (!logUnits.isEmpty()) {
+ publishLogUnitsAtFrontOfBuffer();
+ }
+ }
+
+ @Override
+ protected final void onBufferFull() {
+ publishLogUnitsAtFrontOfBuffer();
+ }
+
+ protected final void publishLogUnitsAtFrontOfBuffer() {
+ ArrayList<LogUnit> logUnits = peekAtFirstNWords(N_GRAM_SIZE);
+ if (isSafeNGram(logUnits, N_GRAM_SIZE)) {
+ // Good n-gram at the front of the buffer. Publish it, disclosing details.
+ publish(logUnits, true /* canIncludePrivateData */);
+ shiftOutWords(N_GRAM_SIZE);
+ resetWordCounter();
+ } else {
+ // No good n-gram at front, and buffer is full. Shift out the first word (or if there
+ // is none, the existing logUnits).
+ logUnits = peekAtFirstNWords(1);
+ publish(logUnits, false /* canIncludePrivateData */);
+ shiftOutWords(1);
+ }
}
+ /**
+ * Called when a list of logUnits should be published.
+ *
+ * It is the subclass's responsibility to implement the publication.
+ *
+ * @param logUnits The list of logUnits to be published.
+ * @param canIncludePrivateData Whether the private data in the logUnits can be included in
+ * publication.
+ */
+ protected abstract void publish(final ArrayList<LogUnit> logUnits,
+ final boolean canIncludePrivateData);
+
@Override
- protected void onShiftOut(LogUnit logUnit) {
- if (mResearchLog != null) {
- mResearchLog.publish(logUnit, false /* isIncludingPrivateData */);
+ protected void shiftOutWords(final int numWords) {
+ final int oldNumActualWords = getNumActualWords();
+ super.shiftOutWords(numWords);
+ final int numWordsShifted = oldNumActualWords - getNumActualWords();
+ mNumWordsUntilSafeToSample -= numWordsShifted;
+ if (DEBUG) {
+ Log.d(TAG, "wordsUntilSafeToSample now at " + mNumWordsUntilSafeToSample);
}
}
}
diff --git a/java/src/com/android/inputmethod/research/MotionEventReader.java b/java/src/com/android/inputmethod/research/MotionEventReader.java
new file mode 100644
index 000000000..e59adfa19
--- /dev/null
+++ b/java/src/com/android/inputmethod/research/MotionEventReader.java
@@ -0,0 +1,332 @@
+/*
+ * 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 android.view.MotionEvent.PointerCoords;
+import android.view.MotionEvent.PointerProperties;
+
+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;
+ // Assumes that MotionEvent.ACTION_MASK does not have all bits set.`
+ private static final int UNINITIALIZED_ACTION = ~MotionEvent.ACTION_MASK;
+ // No legitimate int is negative
+ private static final int UNINITIALIZED_INT = -1;
+ // No legitimate long is negative
+ private static final long UNINITIALIZED_LONG = -1L;
+ // No legitimate float is negative
+ private static final float UNINITIALIZED_FLOAT = -1.0f;
+
+ 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<PointerProperties[]> mPointerPropertiesArrays
+ = new ArrayList<PointerProperties[]>();
+ final ArrayList<PointerCoords[]> mPointerCoordsArrays = new ArrayList<PointerCoords[]>();
+ final ArrayList<Long> mTimes = new ArrayList<Long>();
+ }
+
+ /**
+ * Read motion data from a logStatement and store it in {@code replayData}.
+ *
+ * Two kinds of logStatements can be read. In the first variant, the MotionEvent data is
+ * represented as attributes at the top level like so:
+ *
+ * <pre>
+ * {
+ * "_ct": 1359590400000,
+ * "_ut": 4381933,
+ * "_ty": "MotionEvent",
+ * "action": "UP",
+ * "isLoggingRelated": false,
+ * "x": 100,
+ * "y": 200
+ * }
+ * </pre>
+ *
+ * In the second variant, there is a separate attribute for the MotionEvent that includes
+ * historical data if present:
+ *
+ * <pre>
+ * {
+ * "_ct": 135959040000,
+ * "_ut": 4382702,
+ * "_ty": "MotionEvent",
+ * "action": "MOVE",
+ * "isLoggingRelated": false,
+ * "motionEvent": {
+ * "pointerIds": [
+ * 0
+ * ],
+ * "xyt": [
+ * {
+ * "t": 4382551,
+ * "d": [
+ * {
+ * "x": 141.25,
+ * "y": 151.8485107421875,
+ * "toma": 101.82337188720703,
+ * "tomi": 101.82337188720703,
+ * "o": 0.0
+ * }
+ * ]
+ * },
+ * {
+ * "t": 4382559,
+ * "d": [
+ * {
+ * "x": 140.7266082763672,
+ * "y": 151.8485107421875,
+ * "toma": 101.82337188720703,
+ * "tomi": 101.82337188720703,
+ * "o": 0.0
+ * }
+ * ]
+ * }
+ * ]
+ * }
+ * },
+ * </pre>
+ */
+ /* package for test */ void readLogStatement(final JsonReader jsonReader,
+ final ReplayData replayData) throws IOException {
+ String logStatementType = null;
+ int actionType = UNINITIALIZED_ACTION;
+ int x = UNINITIALIZED_INT;
+ int y = UNINITIALIZED_INT;
+ long time = UNINITIALIZED_LONG;
+ boolean isLoggingRelated = 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")) {
+ isLoggingRelated = jsonReader.nextBoolean();
+ } else if (logStatementType != null && logStatementType.equals("MotionEvent")
+ && key.equals("motionEvent")) {
+ if (actionType == UNINITIALIZED_ACTION) {
+ Log.e(TAG, "no actionType assigned in MotionEvent json");
+ }
+ // Second variant of LogStatement.
+ if (isLoggingRelated) {
+ jsonReader.skipValue();
+ } else {
+ readEmbeddedMotionEvent(jsonReader, replayData, actionType);
+ }
+ } else {
+ if (DEBUG) {
+ Log.w(TAG, "Unknown JSON key in LogStatement: " + key);
+ }
+ jsonReader.skipValue();
+ }
+ }
+ jsonReader.endObject();
+
+ if (logStatementType != null && time != UNINITIALIZED_LONG && x != UNINITIALIZED_INT
+ && y != UNINITIALIZED_INT && actionType != UNINITIALIZED_ACTION
+ && logStatementType.equals("MotionEvent") && !isLoggingRelated) {
+ // First variant of LogStatement.
+ final PointerProperties pointerProperties = new PointerProperties();
+ pointerProperties.id = 0;
+ pointerProperties.toolType = MotionEvent.TOOL_TYPE_UNKNOWN;
+ final PointerProperties[] pointerPropertiesArray = {
+ pointerProperties
+ };
+ final PointerCoords pointerCoords = new PointerCoords();
+ pointerCoords.x = x;
+ pointerCoords.y = y;
+ pointerCoords.pressure = 1.0f;
+ pointerCoords.size = 1.0f;
+ final PointerCoords[] pointerCoordsArray = {
+ pointerCoords
+ };
+ addMotionEventData(replayData, actionType, time, pointerPropertiesArray,
+ pointerCoordsArray);
+ }
+ }
+
+ private void readEmbeddedMotionEvent(final JsonReader jsonReader, final ReplayData replayData,
+ final int actionType) throws IOException {
+ jsonReader.beginObject();
+ PointerProperties[] pointerPropertiesArray = null;
+ while (jsonReader.hasNext()) { // pointerIds/xyt
+ final String name = jsonReader.nextName();
+ if (name.equals("pointerIds")) {
+ pointerPropertiesArray = readPointerProperties(jsonReader);
+ } else if (name.equals("xyt")) {
+ readPointerData(jsonReader, replayData, actionType, pointerPropertiesArray);
+ }
+ }
+ jsonReader.endObject();
+ }
+
+ private PointerProperties[] readPointerProperties(final JsonReader jsonReader)
+ throws IOException {
+ final ArrayList<PointerProperties> pointerPropertiesArrayList =
+ new ArrayList<PointerProperties>();
+ jsonReader.beginArray();
+ while (jsonReader.hasNext()) {
+ final PointerProperties pointerProperties = new PointerProperties();
+ pointerProperties.id = jsonReader.nextInt();
+ pointerProperties.toolType = MotionEvent.TOOL_TYPE_UNKNOWN;
+ pointerPropertiesArrayList.add(pointerProperties);
+ }
+ jsonReader.endArray();
+ return pointerPropertiesArrayList.toArray(
+ new PointerProperties[pointerPropertiesArrayList.size()]);
+ }
+
+ private void readPointerData(final JsonReader jsonReader, final ReplayData replayData,
+ final int actionType, final PointerProperties[] pointerPropertiesArray)
+ throws IOException {
+ if (pointerPropertiesArray == null) {
+ Log.e(TAG, "PointerIDs must be given before xyt data in json for MotionEvent");
+ jsonReader.skipValue();
+ return;
+ }
+ long time = UNINITIALIZED_LONG;
+ jsonReader.beginArray();
+ while (jsonReader.hasNext()) { // Array of historical data
+ jsonReader.beginObject();
+ final ArrayList<PointerCoords> pointerCoordsArrayList = new ArrayList<PointerCoords>();
+ while (jsonReader.hasNext()) { // Time/data object
+ final String name = jsonReader.nextName();
+ if (name.equals("t")) {
+ time = jsonReader.nextLong();
+ } else if (name.equals("d")) {
+ jsonReader.beginArray();
+ while (jsonReader.hasNext()) { // array of data per pointer
+ final PointerCoords pointerCoords = readPointerCoords(jsonReader);
+ if (pointerCoords != null) {
+ pointerCoordsArrayList.add(pointerCoords);
+ }
+ }
+ jsonReader.endArray();
+ } else {
+ jsonReader.skipValue();
+ }
+ }
+ jsonReader.endObject();
+ // Data was recorded as historical events, but must be split apart into
+ // separate MotionEvents for replaying
+ if (time != UNINITIALIZED_LONG) {
+ addMotionEventData(replayData, actionType, time, pointerPropertiesArray,
+ pointerCoordsArrayList.toArray(
+ new PointerCoords[pointerCoordsArrayList.size()]));
+ } else {
+ Log.e(TAG, "Time not assigned in json for MotionEvent");
+ }
+ }
+ jsonReader.endArray();
+ }
+
+ private PointerCoords readPointerCoords(final JsonReader jsonReader) throws IOException {
+ jsonReader.beginObject();
+ float x = UNINITIALIZED_FLOAT;
+ float y = UNINITIALIZED_FLOAT;
+ while (jsonReader.hasNext()) { // x,y
+ final String name = jsonReader.nextName();
+ if (name.equals("x")) {
+ x = (float) jsonReader.nextDouble();
+ } else if (name.equals("y")) {
+ y = (float) jsonReader.nextDouble();
+ } else {
+ jsonReader.skipValue();
+ }
+ }
+ jsonReader.endObject();
+
+ if (Float.compare(x, UNINITIALIZED_FLOAT) == 0
+ || Float.compare(y, UNINITIALIZED_FLOAT) == 0) {
+ Log.w(TAG, "missing x or y value in MotionEvent json");
+ return null;
+ }
+ final PointerCoords pointerCoords = new PointerCoords();
+ pointerCoords.x = x;
+ pointerCoords.y = y;
+ pointerCoords.pressure = 1.0f;
+ pointerCoords.size = 1.0f;
+ return pointerCoords;
+ }
+
+ /**
+ * Tests that {@code x} is uninitialized.
+ *
+ * Assumes that {@code x} will never be given a valid value less than 0, and that
+ * UNINITIALIZED_FLOAT is less than 0.0f.
+ */
+ private boolean isUninitializedFloat(final float x) {
+ return x < 0.0f;
+ }
+
+ private void addMotionEventData(final ReplayData replayData, final int actionType,
+ final long time, final PointerProperties[] pointerProperties,
+ final PointerCoords[] pointerCoords) {
+ replayData.mActions.add(actionType);
+ replayData.mTimes.add(time);
+ replayData.mPointerPropertiesArrays.add(pointerProperties);
+ replayData.mPointerCoordsArrays.add(pointerCoords);
+ }
+}
diff --git a/java/src/com/android/inputmethod/research/Replayer.java b/java/src/com/android/inputmethod/research/Replayer.java
new file mode 100644
index 000000000..a9b7a9d0c
--- /dev/null
+++ b/java/src/com/android/inputmethod/research/Replayer.java
@@ -0,0 +1,149 @@
+/*
+ * 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.Looper;
+import android.os.Message;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.MotionEvent.PointerCoords;
+import android.view.MotionEvent.PointerProperties;
+
+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;
+
+ private Replayer() {
+ }
+
+ private static final Replayer sInstance = new Replayer();
+ public static Replayer getInstance() {
+ return sInstance;
+ }
+
+ 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, final Runnable callback) {
+ 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(Looper.getMainLooper()) {
+ // 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 PointerProperties[] pointerPropertiesArray =
+ replayData.mPointerPropertiesArrays.get(index);
+ final PointerCoords[] pointerCoordsArray =
+ replayData.mPointerCoordsArrays.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,
+ pointerPropertiesArray.length, pointerPropertiesArray,
+ pointerCoordsArray, 0, 0, 1.0f, 1.0f, 0, 0, 0, 0);
+ mainKeyboardView.processMotionEvent(me);
+ me.recycle();
+ break;
+ case MSG_DONE:
+ mIsReplaying = false;
+ ResearchLogger.getInstance().requestIndicatorRedraw();
+ break;
+ }
+ }
+ };
+
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ ResearchLogger.getInstance().requestIndicatorRedraw();
+ }
+ });
+ 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);
+ if (callback != null) {
+ handler.postAtTime(callback, presentDoneTime + 1);
+ }
+ }
+
+ public boolean isReplaying() {
+ return mIsReplaying;
+ }
+}
diff --git a/java/src/com/android/inputmethod/research/ReplayerService.java b/java/src/com/android/inputmethod/research/ReplayerService.java
new file mode 100644
index 000000000..88d9033cf
--- /dev/null
+++ b/java/src/com/android/inputmethod/research/ReplayerService.java
@@ -0,0 +1,65 @@
+/*
+ * 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.app.IntentService;
+import android.content.Intent;
+import android.util.Log;
+
+import com.android.inputmethod.research.MotionEventReader.ReplayData;
+
+import java.io.File;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Provide a mechanism to invoke the replayer from outside.
+ *
+ * In particular, makes access from a host possible through {@code adb am startservice}.
+ */
+public class ReplayerService extends IntentService {
+ private static final String TAG = ReplayerService.class.getSimpleName();
+ private static final String EXTRA_FILENAME = "com.android.inputmethod.research.extra.FILENAME";
+ private static final long MAX_REPLAY_TIME = TimeUnit.SECONDS.toMillis(60);
+
+ public ReplayerService() {
+ super(ReplayerService.class.getSimpleName());
+ }
+
+ @Override
+ protected void onHandleIntent(final Intent intent) {
+ final String filename = intent.getStringExtra(EXTRA_FILENAME);
+ if (filename == null) return;
+
+ final ReplayData replayData = new MotionEventReader().readMotionEventData(
+ new File(filename));
+ synchronized (this) {
+ Replayer.getInstance().replay(replayData, new Runnable() {
+ @Override
+ public void run() {
+ synchronized (ReplayerService.this) {
+ ReplayerService.this.notify();
+ }
+ }
+ });
+ try {
+ wait(MAX_REPLAY_TIME);
+ } catch (InterruptedException e) {
+ Log.e(TAG, "Timeout while replaying.", e);
+ }
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/research/ResearchLog.java b/java/src/com/android/inputmethod/research/ResearchLog.java
index 70c38e909..99d84938f 100644
--- a/java/src/com/android/inputmethod/research/ResearchLog.java
+++ b/java/src/com/android/inputmethod/research/ResearchLog.java
@@ -1,39 +1,33 @@
/*
* 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
+ * 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
+ * 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.
+ * 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.content.SharedPreferences;
-import android.os.SystemClock;
+import android.content.Context;
import android.util.JsonWriter;
import android.util.Log;
-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.define.ProductionFlag;
import java.io.BufferedWriter;
import java.io.File;
-import java.io.FileWriter;
+import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
-import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
@@ -51,12 +45,14 @@ import java.util.concurrent.TimeUnit;
*/
public class ResearchLog {
private static final String TAG = ResearchLog.class.getSimpleName();
- private static final boolean DEBUG = false;
+ private static final boolean DEBUG = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG;
private static final long FLUSH_DELAY_IN_MS = 1000 * 5;
private static final int ABORT_TIMEOUT_IN_MS = 1000 * 4;
/* package */ final ScheduledExecutorService mExecutor;
/* package */ final File mFile;
+ private final Context mContext;
+
private JsonWriter mJsonWriter = NULL_JSON_WRITER;
// true if at least one byte of data has been written out to the log file. This must be
// remembered because JsonWriter requires that calls matching calls to beginObject and
@@ -85,12 +81,10 @@ public class ResearchLog {
}
}
- public ResearchLog(final File outputFile) {
- if (outputFile == null) {
- throw new IllegalArgumentException();
- }
+ public ResearchLog(final File outputFile, final Context context) {
mExecutor = Executors.newSingleThreadScheduledExecutor();
mFile = outputFile;
+ mContext = context;
}
public synchronized void close(final Runnable onClosed) {
@@ -115,7 +109,7 @@ public class ResearchLog {
Log.d(TAG, "error when closing ResearchLog:");
e.printStackTrace();
} finally {
- if (mFile.exists()) {
+ if (mFile != null && mFile.exists()) {
mFile.setWritable(false, false);
}
if (onClosed != null) {
@@ -142,7 +136,9 @@ public class ResearchLog {
mHasWrittenData = false;
}
} finally {
- mIsAbortSuccessful = mFile.delete();
+ if (mFile != null) {
+ mIsAbortSuccessful = mFile.delete();
+ }
}
return null;
}
@@ -188,133 +184,61 @@ public class ResearchLog {
mFlushFuture = mExecutor.schedule(mFlushCallable, FLUSH_DELAY_IN_MS, TimeUnit.MILLISECONDS);
}
- public synchronized void publish(final LogUnit logUnit, final boolean isIncludingPrivateData) {
+ public synchronized void publish(final LogUnit logUnit, final boolean canIncludePrivateData) {
try {
mExecutor.submit(new Callable<Object>() {
@Override
public Object call() throws Exception {
- logUnit.publishTo(ResearchLog.this, isIncludingPrivateData);
+ logUnit.publishTo(ResearchLog.this, canIncludePrivateData);
scheduleFlush();
return null;
}
});
} catch (RejectedExecutionException e) {
// TODO: Add code to record loss of data, and report.
+ if (DEBUG) {
+ Log.d(TAG, "ResearchLog.publish() rejecting scheduled execution");
+ }
}
}
- private static final String CURRENT_TIME_KEY = "_ct";
- private static final String UPTIME_KEY = "_ut";
- private static final String EVENT_TYPE_KEY = "_ty";
-
- void outputEvent(final String[] keys, final Object[] values) {
- // Not thread safe.
- if (keys.length == 0) {
- return;
- }
- if (DEBUG) {
- if (keys.length != values.length + 1) {
- Log.d(TAG, "Key and Value list sizes do not match. " + keys[0]);
- }
- }
+ /**
+ * Return a JsonWriter for this ResearchLog. It is initialized the first time this method is
+ * called. The cached value is returned in future calls.
+ */
+ public JsonWriter getInitializedJsonWriterLocked() {
+ if (mJsonWriter != NULL_JSON_WRITER || mFile == null) return mJsonWriter;
try {
- if (mJsonWriter == NULL_JSON_WRITER) {
- mJsonWriter = new JsonWriter(new BufferedWriter(new FileWriter(mFile)));
- mJsonWriter.beginArray();
+ final JsonWriter jsonWriter = createJsonWriter(mContext, mFile);
+ if (jsonWriter != null) {
+ jsonWriter.beginArray();
+ mJsonWriter = jsonWriter;
mHasWrittenData = true;
}
- mJsonWriter.beginObject();
- mJsonWriter.name(CURRENT_TIME_KEY).value(System.currentTimeMillis());
- mJsonWriter.name(UPTIME_KEY).value(SystemClock.uptimeMillis());
- mJsonWriter.name(EVENT_TYPE_KEY).value(keys[0]);
- final int length = values.length;
- for (int i = 0; i < length; i++) {
- mJsonWriter.name(keys[i + 1]);
- Object value = values[i];
- if (value instanceof CharSequence) {
- mJsonWriter.value(value.toString());
- } else if (value instanceof Number) {
- mJsonWriter.value((Number) value);
- } else if (value instanceof Boolean) {
- mJsonWriter.value((Boolean) value);
- } else if (value instanceof CompletionInfo[]) {
- CompletionInfo[] ci = (CompletionInfo[]) value;
- mJsonWriter.beginArray();
- for (int j = 0; j < ci.length; j++) {
- mJsonWriter.value(ci[j].toString());
- }
- mJsonWriter.endArray();
- } else if (value instanceof SharedPreferences) {
- SharedPreferences prefs = (SharedPreferences) value;
- mJsonWriter.beginObject();
- for (Map.Entry<String,?> entry : prefs.getAll().entrySet()) {
- mJsonWriter.name(entry.getKey());
- final Object innerValue = entry.getValue();
- if (innerValue == null) {
- mJsonWriter.nullValue();
- } else if (innerValue instanceof Boolean) {
- mJsonWriter.value((Boolean) innerValue);
- } else if (innerValue instanceof Number) {
- mJsonWriter.value((Number) innerValue);
- } else {
- mJsonWriter.value(innerValue.toString());
- }
- }
- mJsonWriter.endObject();
- } else if (value instanceof Key[]) {
- Key[] keyboardKeys = (Key[]) value;
- mJsonWriter.beginArray();
- for (Key keyboardKey : keyboardKeys) {
- mJsonWriter.beginObject();
- mJsonWriter.name("code").value(keyboardKey.mCode);
- mJsonWriter.name("altCode").value(keyboardKey.getAltCode());
- mJsonWriter.name("x").value(keyboardKey.mX);
- mJsonWriter.name("y").value(keyboardKey.mY);
- mJsonWriter.name("w").value(keyboardKey.mWidth);
- mJsonWriter.name("h").value(keyboardKey.mHeight);
- mJsonWriter.endObject();
- }
- mJsonWriter.endArray();
- } else if (value instanceof SuggestedWords) {
- SuggestedWords words = (SuggestedWords) value;
- mJsonWriter.beginObject();
- mJsonWriter.name("typedWordValid").value(words.mTypedWordValid);
- mJsonWriter.name("willAutoCorrect").value(words.mWillAutoCorrect);
- mJsonWriter.name("isPunctuationSuggestions")
- .value(words.mIsPunctuationSuggestions);
- mJsonWriter.name("isObsoleteSuggestions").value(words.mIsObsoleteSuggestions);
- mJsonWriter.name("isPrediction").value(words.mIsPrediction);
- mJsonWriter.name("words");
- mJsonWriter.beginArray();
- final int size = words.size();
- for (int j = 0; j < size; j++) {
- SuggestedWordInfo wordInfo = words.getWordInfo(j);
- mJsonWriter.value(wordInfo.toString());
- }
- mJsonWriter.endArray();
- mJsonWriter.endObject();
- } else if (value == null) {
- mJsonWriter.nullValue();
- } else {
- Log.w(TAG, "Unrecognized type to be logged: " +
- (value == null ? "<null>" : value.getClass().getName()));
- mJsonWriter.nullValue();
- }
- }
- mJsonWriter.endObject();
- } catch (IOException e) {
- e.printStackTrace();
- Log.w(TAG, "Error in JsonWriter; disabling logging");
+ } catch (final IOException e) {
+ Log.w(TAG, "Error in JsonWriter; disabling logging", e);
try {
mJsonWriter.close();
- } catch (IllegalStateException e1) {
+ } catch (final IllegalStateException e1) {
// Assume that this is just the json not being terminated properly.
// Ignore
- } catch (IOException e1) {
- e1.printStackTrace();
+ } catch (final IOException e1) {
+ Log.w(TAG, "Error in closing JsonWriter; disabling logging", e1);
} finally {
mJsonWriter = NULL_JSON_WRITER;
}
}
+ return mJsonWriter;
+ }
+
+ /**
+ * Create the JsonWriter to write the ResearchLog to.
+ *
+ * This method may be overriden in testing to redirect the output.
+ */
+ /* package for test */ JsonWriter createJsonWriter(final Context context, final File file)
+ throws IOException {
+ return new JsonWriter(new BufferedWriter(new OutputStreamWriter(
+ context.openFileOutput(file.getName(), Context.MODE_PRIVATE))));
}
}
diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java
index 763fd6e00..25633d630 100644
--- a/java/src/com/android/inputmethod/research/ResearchLogger.java
+++ b/java/src/com/android/inputmethod/research/ResearchLogger.java
@@ -1,23 +1,25 @@
/*
* 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
+ * 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
+ * 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.
+ * 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 static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET;
+import android.accounts.Account;
+import android.accounts.AccountManager;
import android.app.AlarmManager;
import android.app.AlertDialog;
import android.app.Dialog;
@@ -30,15 +32,18 @@ import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
-import android.inputmethodservice.InputMethodService;
import android.net.Uri;
import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
import android.os.IBinder;
import android.os.SystemClock;
+import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.Log;
@@ -47,7 +52,6 @@ import android.view.MotionEvent;
import android.view.Window;
import android.view.WindowManager;
import android.view.inputmethod.CompletionInfo;
-import android.view.inputmethod.CorrectionInfo;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.widget.Toast;
@@ -55,11 +59,12 @@ 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.CollectionUtils;
import com.android.inputmethod.latin.Constants;
import com.android.inputmethod.latin.Dictionary;
+import com.android.inputmethod.latin.InputTypeUtils;
import com.android.inputmethod.latin.LatinIME;
import com.android.inputmethod.latin.R;
import com.android.inputmethod.latin.RichInputConnection;
@@ -67,11 +72,23 @@ import com.android.inputmethod.latin.RichInputConnection.Range;
import com.android.inputmethod.latin.Suggest;
import com.android.inputmethod.latin.SuggestedWords;
import com.android.inputmethod.latin.define.ProductionFlag;
+import com.android.inputmethod.research.MotionEventReader.ReplayData;
+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.nio.MappedByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.charset.Charset;
import java.text.SimpleDateFormat;
+import java.util.ArrayList;
import java.util.Date;
+import java.util.List;
import java.util.Locale;
+import java.util.Random;
import java.util.UUID;
/**
@@ -83,28 +100,66 @@ import java.util.UUID;
* This functionality is off by default. See {@link ProductionFlag#IS_EXPERIMENTAL}.
*/
public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChangeListener {
+ // TODO: This class has grown quite large and combines several concerns that should be
+ // separated. The following refactorings will be applied as soon as possible after adding
+ // support for replaying historical events, fixing some replay bugs, adding some ui constraints
+ // on the feedback dialog, and adding the survey dialog.
+ // TODO: Refactor. Move splash screen code into separate class.
+ // TODO: Refactor. Move feedback screen code into separate class.
+ // TODO: Refactor. Move logging invocations into their own class.
+ // TODO: Refactor. Move currentLogUnit management into separate class.
private static final String TAG = ResearchLogger.class.getSimpleName();
- private static final boolean DEBUG = false;
- private static final boolean OUTPUT_ENTIRE_BUFFER = false; // true may disclose private info
+ private static final boolean DEBUG = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG;
+ private static final boolean DEBUG_REPLAY_AFTER_FEEDBACK = false
+ && ProductionFlag.IS_EXPERIMENTAL_DEBUG;
+ // Whether the TextView contents are logged at the end of the session. true will disclose
+ // private info.
+ private static final boolean LOG_FULL_TEXTVIEW_CONTENTS = false
+ && ProductionFlag.IS_EXPERIMENTAL_DEBUG;
+ // Whether the feedback dialog preserves the editable text across invocations. Should be false
+ // for normal research builds so users do not have to delete the same feedback string they
+ // entered earlier. Should be true for builds internal to a development team so when the text
+ // field holds a channel name, the developer does not have to re-enter it when using the
+ // feedback mechanism to generate multiple tests.
+ private static final boolean FEEDBACK_DIALOG_SHOULD_PRESERVE_TEXT_FIELD = false;
public static final boolean DEFAULT_USABILITY_STUDY_MODE = false;
/* package */ static boolean sIsLogging = false;
- private static final int OUTPUT_FORMAT_VERSION = 1;
+ 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 all words should be recorded, leaving unsampled word between bigrams. Useful for
+ // testing.
+ /* package for test */ static final boolean IS_LOGGING_EVERYTHING = false
+ && ProductionFlag.IS_EXPERIMENTAL_DEBUG;
+ // The number of words between n-grams to omit from the log.
+ private static final int NUMBER_OF_WORDS_BETWEEN_SAMPLES =
+ IS_LOGGING_EVERYTHING ? 0 : (DEBUG ? 2 : 18);
+
+ // Whether to show an indicator on the screen that logging is on. Currently a very small red
+ // dot in the lower right hand corner. Most users should not notice it.
private static final boolean IS_SHOWING_INDICATOR = true;
- private static final boolean IS_SHOWING_INDICATOR_CLEARLY = false;
- public static final int FEEDBACK_WORD_BUFFER_SIZE = 5;
+ // Change the default indicator to something very visible. Currently two red vertical bars on
+ // either side of they keyboard.
+ private static final boolean IS_SHOWING_INDICATOR_CLEARLY = false ||
+ (IS_LOGGING_EVERYTHING && ProductionFlag.IS_EXPERIMENTAL_DEBUG);
+ // FEEDBACK_WORD_BUFFER_SIZE should add 1 because it must also hold the feedback LogUnit itself.
+ public static final int FEEDBACK_WORD_BUFFER_SIZE = (Integer.MAX_VALUE - 1) + 1;
// constants related to specific log points
private static final String WHITESPACE_SEPARATORS = " \t\n\r";
private static final int MAX_INPUTVIEW_LENGTH_TO_CAPTURE = 8192; // must be >=1
private static final String PREF_RESEARCH_LOGGER_UUID_STRING = "pref_research_logger_uuid";
+ private static final String PREF_RESEARCH_SAVED_CHANNEL = "pref_research_saved_channel";
private static final ResearchLogger sInstance = new ResearchLogger();
+ private static String sAccountType = null;
+ private static String sAllowedAccountDomain = null;
// to write to a different filename, e.g., for testing, set mFile before calling start()
/* package */ File mFilesDir;
/* package */ String mUUIDString;
@@ -114,9 +169,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;
@@ -129,7 +190,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
/* package for test */ static final String WORD_REPLACEMENT_STRING = "\uE001";
private static final String PREF_LAST_CLEANUP_TIME = "pref_last_cleanup_time";
private static final long DURATION_BETWEEN_DIR_CLEANUP_IN_MS = DateUtils.DAY_IN_MILLIS;
- private static final long MAX_LOGFILE_AGE_IN_MS = DateUtils.DAY_IN_MILLIS;
+ private static final long MAX_LOGFILE_AGE_IN_MS = 4 * DateUtils.DAY_IN_MILLIS;
protected static final int SUSPEND_DURATION_IN_MINUTES = 1;
// set when LatinIME should ignore an onUpdateSelection() callback that
// arises from operations in this class
@@ -137,16 +198,37 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
// used to check whether words are not unique
private Suggest mSuggest;
- private Dictionary mDictionary;
private MainKeyboardView mMainKeyboardView;
- private InputMethodService mInputMethodService;
+ // TODO: Check whether a superclass can be used instead of LatinIME.
+ /* package for test */ LatinIME mLatinIME;
private final Statistics mStatistics;
+ private final MotionEventReader mMotionEventReader = new MotionEventReader();
+ private final Replayer mReplayer = Replayer.getInstance();
private Intent mUploadIntent;
- private PendingIntent mUploadPendingIntent;
+ private Intent mUploadNowIntent;
private LogUnit mCurrentLogUnit = new LogUnit();
+ // Gestured or tapped words may be committed after the gesture of the next word has started.
+ // To ensure that the gesture data of the next word is not associated with the previous word,
+ // thereby leaking private data, we store the time of the down event that started the second
+ // gesture, and when committing the earlier word, split the LogUnit.
+ private long mSavedDownEventTime;
+ private Bundle mFeedbackDialogBundle = null;
+ private boolean mInFeedbackDialog = false;
+ // The feedback dialog causes stop() to be called for the keyboard connected to the original
+ // window. This is because the feedback dialog must present its own EditText box that displays
+ // a keyboard. stop() normally causes mFeedbackLogBuffer, which contains the user's data, to be
+ // cleared, and causes mFeedbackLog, which is ready to collect information in case the user
+ // wants to upload, to be closed. This is good because we don't need to log information about
+ // what the user is typing in the feedback dialog, but bad because this data must be uploaded.
+ // Here we save the LogBuffer and Log so the feedback dialog can later access their data.
+ private LogBuffer mSavedFeedbackLogBuffer;
+ private ResearchLog mSavedFeedbackLog;
+ private Handler mUserRecordingTimeoutHandler;
+ private static final long USER_RECORDING_TIMEOUT_MS = 30L * DateUtils.SECOND_IN_MILLIS;
+
private ResearchLogger() {
mStatistics = Statistics.getInstance();
}
@@ -155,16 +237,17 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
return sInstance;
}
- public void init(final InputMethodService ims, final SharedPreferences prefs) {
- assert ims != null;
- if (ims == null) {
+ public void init(final LatinIME latinIME, final KeyboardSwitcher keyboardSwitcher) {
+ assert latinIME != null;
+ if (latinIME == null) {
Log.w(TAG, "IMS is null; logging is off");
} else {
- mFilesDir = ims.getFilesDir();
+ mFilesDir = latinIME.getFilesDir();
if (mFilesDir == null || !mFilesDir.exists()) {
Log.w(TAG, "IME storage directory does not exist.");
}
}
+ final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(latinIME);
if (prefs != null) {
mUUIDString = getUUID(prefs);
if (!prefs.contains(PREF_USABILITY_STUDY_MODE)) {
@@ -185,13 +268,18 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
e.apply();
}
}
- mInputMethodService = ims;
+ final Resources res = latinIME.getResources();
+ sAccountType = res.getString(R.string.research_account_type);
+ sAllowedAccountDomain = res.getString(R.string.research_allowed_account_domain);
+ mLatinIME = latinIME;
mPrefs = prefs;
- mUploadIntent = new Intent(mInputMethodService, UploaderService.class);
- mUploadPendingIntent = PendingIntent.getService(mInputMethodService, 0, mUploadIntent, 0);
+ mUploadIntent = new Intent(mLatinIME, UploaderService.class);
+ mUploadNowIntent = new Intent(mLatinIME, UploaderService.class);
+ mUploadNowIntent.putExtra(UploaderService.EXTRA_UPLOAD_UNCONDITIONALLY, true);
+ mReplayer.setKeyboardSwitcher(keyboardSwitcher);
if (ProductionFlag.IS_EXPERIMENTAL) {
- scheduleUploadingService(mInputMethodService);
+ scheduleUploadingService(mLatinIME);
}
}
@@ -216,8 +304,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();
}
}
@@ -250,7 +340,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
if (windowToken == null) {
return;
}
- final AlertDialog.Builder builder = new AlertDialog.Builder(mInputMethodService)
+ final AlertDialog.Builder builder = new AlertDialog.Builder(mLatinIME)
.setTitle(R.string.research_splash_title)
.setMessage(R.string.research_splash_content)
.setPositiveButton(android.R.string.yes,
@@ -265,12 +355,12 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
- final String packageName = mInputMethodService.getPackageName();
+ final String packageName = mLatinIME.getPackageName();
final Uri packageUri = Uri.parse("package:" + packageName);
final Intent intent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE,
packageUri);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- mInputMethodService.startActivity(intent);
+ mLatinIME.startActivity(intent);
}
})
.setCancelable(true)
@@ -278,7 +368,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
new OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
- mInputMethodService.requestHideSelf(0);
+ mLatinIME.requestHideSelf(0);
}
});
mSplashDialog = builder.create();
@@ -312,20 +402,40 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
sIsLogging = enableLogging;
}
- private File createLogFile(File filesDir) {
+ private static int sLogFileCounter = 0;
+
+ 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
+ // separate these.
+ if (sLogFileCounter < Integer.MAX_VALUE) {
+ sLogFileCounter++;
+ } else {
+ // Wrap the counter, in the unlikely event of overflow.
+ sLogFileCounter = 0;
+ }
+ sb.append(sLogFileCounter);
+ sb.append(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(FILENAME_SUFFIX);
+ sb.append(USER_RECORDING_FILENAME_SUFFIX);
return new File(filesDir, sb.toString());
}
private void checkForEmptyEditor() {
- if (mInputMethodService == null) {
+ if (mLatinIME == null) {
return;
}
- final InputConnection ic = mInputMethodService.getCurrentInputConnection();
+ final InputConnection ic = mLatinIME.getCurrentInputConnection();
if (ic == null) {
return;
}
@@ -362,27 +472,56 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
return;
}
if (mMainLogBuffer == null) {
- mMainResearchLog = new ResearchLog(createLogFile(mFilesDir));
- mMainLogBuffer = new MainLogBuffer(mMainResearchLog);
+ mMainResearchLog = new ResearchLog(createLogFile(mFilesDir), mLatinIME);
+ final int numWordsToIgnore = new Random().nextInt(NUMBER_OF_WORDS_BETWEEN_SAMPLES + 1);
+ mMainLogBuffer = new MainLogBuffer(NUMBER_OF_WORDS_BETWEEN_SAMPLES, numWordsToIgnore) {
+ @Override
+ protected void publish(final ArrayList<LogUnit> logUnits,
+ boolean canIncludePrivateData) {
+ canIncludePrivateData |= IS_LOGGING_EVERYTHING;
+ final int length = logUnits.size();
+ for (int i = 0; i < length; i++) {
+ final LogUnit logUnit = logUnits.get(i);
+ final String word = logUnit.getWord();
+ if (word != null && word.length() > 0 && hasLetters(word)) {
+ Log.d(TAG, "onPublish: " + word + ", hc: "
+ + logUnit.containsCorrection());
+ final Dictionary dictionary = getDictionary();
+ mStatistics.recordWordEntered(
+ dictionary != null && dictionary.isValidWord(word),
+ logUnit.containsCorrection());
+ }
+ }
+ if (mMainResearchLog != null) {
+ publishLogUnits(logUnits, mMainResearchLog, canIncludePrivateData);
+ }
+ }
+ };
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);
+ resetFeedbackLogging();
}
}
+ private void resetFeedbackLogging() {
+ mFeedbackLog = new ResearchLog(createLogFile(mFilesDir), mLatinIME);
+ mFeedbackLogBuffer = new FixedLogBuffer(FEEDBACK_WORD_BUFFER_SIZE);
+ }
+
/* package */ void stop() {
if (DEBUG) {
Log.d(TAG, "stop called");
}
- logStatistics();
+ // Commit mCurrentLogUnit before closing.
commitCurrentLogUnit();
if (mMainLogBuffer != null) {
- publishLogBuffer(mMainLogBuffer, mMainResearchLog, false /* isIncludingPrivateData */);
+ mMainLogBuffer.shiftAndPublishAll();
+ logStatistics();
+ commitCurrentLogUnit();
+ mMainLogBuffer.setIsStopping();
+ mMainLogBuffer.shiftAndPublishAll();
mMainResearchLog.close(null /* callback */);
mMainLogBuffer = null;
}
@@ -427,21 +566,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
}
private long mResumeTime = 0L;
- private void suspendLoggingUntil(long time) {
- mIsLoggingSuspended = true;
- mResumeTime = time;
- requestIndicatorRedraw();
- }
-
- private void resumeLogging() {
- mResumeTime = 0L;
- updateSuspendedState();
- requestIndicatorRedraw();
- if (isAllowedToLog()) {
- restart();
- }
- }
-
private void updateSuspendedState() {
final long time = System.currentTimeMillis();
if (time > mResumeTime) {
@@ -472,42 +596,108 @@ 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;
- }
+ public void presentFeedbackDialog(LatinIME latinIME) {
+ if (isMakingUserRecording()) {
+ saveRecording();
+ }
+ mInFeedbackDialog = true;
+ mSavedFeedbackLogBuffer = mFeedbackLogBuffer;
+ mSavedFeedbackLog = mFeedbackLog;
+ // Set the non-saved versions to null so that the stop() caused by switching to the
+ // Feedback dialog will not close them.
+ mFeedbackLogBuffer = null;
+ mFeedbackLog = null;
+
+ final Intent intent = new Intent();
+ intent.setClass(mLatinIME, FeedbackActivity.class);
+ if (mFeedbackDialogBundle == null) {
+ // Restore feedback field with channel name
+ final Bundle bundle = new Bundle();
+ bundle.putBoolean(FeedbackFragment.KEY_INCLUDE_ACCOUNT_NAME, true);
+ bundle.putBoolean(FeedbackFragment.KEY_HAS_USER_RECORDING, false);
+ if (FEEDBACK_DIALOG_SHOULD_PRESERVE_TEXT_FIELD) {
+ final String savedChannelName = mPrefs.getString(PREF_RESEARCH_SAVED_CHANNEL, "");
+ bundle.putString(FeedbackFragment.KEY_FEEDBACK_STRING, savedChannelName);
}
+ mFeedbackDialogBundle = bundle;
+ }
+ intent.putExtras(mFeedbackDialogBundle);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ latinIME.startActivity(intent);
+ }
- };
- final AlertDialog.Builder builder = new AlertDialog.Builder(latinIME)
- .setItems(items, listener)
- .setTitle(title);
- latinIME.showOptionDialog(builder.create());
+ public void setFeedbackDialogBundle(final Bundle bundle) {
+ mFeedbackDialogBundle = bundle;
}
- */
- private boolean mInFeedbackDialog = false;
- public void presentFeedbackDialog(LatinIME latinIME) {
- mInFeedbackDialog = true;
- latinIME.launchKeyboardedDialogActivity(FeedbackActivity.class);
+ public void startRecording() {
+ final Resources res = mLatinIME.getResources();
+ Toast.makeText(mLatinIME,
+ res.getString(R.string.research_feedback_demonstration_instructions),
+ Toast.LENGTH_LONG).show();
+ startRecordingInternal();
+ }
+
+ private void startRecordingInternal() {
+ if (mUserRecordingLog != null) {
+ mUserRecordingLog.abort();
+ }
+ mUserRecordingFile = createUserRecordingFile(mFilesDir);
+ mUserRecordingLog = new ResearchLog(mUserRecordingFile, mLatinIME);
+ mUserRecordingLogBuffer = new LogBuffer();
+ resetRecordingTimer();
+ }
+
+ private boolean isMakingUserRecording() {
+ return mUserRecordingLog != null;
+ }
+
+ private void resetRecordingTimer() {
+ if (mUserRecordingTimeoutHandler == null) {
+ mUserRecordingTimeoutHandler = new Handler();
+ }
+ clearRecordingTimer();
+ mUserRecordingTimeoutHandler.postDelayed(mRecordingHandlerTimeoutRunnable,
+ USER_RECORDING_TIMEOUT_MS);
+ }
+
+ private void clearRecordingTimer() {
+ mUserRecordingTimeoutHandler.removeCallbacks(mRecordingHandlerTimeoutRunnable);
+ }
+
+ private Runnable mRecordingHandlerTimeoutRunnable = new Runnable() {
+ @Override
+ public void run() {
+ cancelRecording();
+ requestIndicatorRedraw();
+ final Resources res = mLatinIME.getResources();
+ Toast.makeText(mLatinIME, res.getString(R.string.research_feedback_recording_failure),
+ Toast.LENGTH_LONG).show();
+ }
+ };
+
+ private void cancelRecording() {
+ if (mUserRecordingLog != null) {
+ mUserRecordingLog.abort();
+ }
+ mUserRecordingLog = null;
+ mUserRecordingLogBuffer = null;
+ if (mFeedbackDialogBundle != null) {
+ mFeedbackDialogBundle.putBoolean("HasRecording", false);
+ }
+ }
+
+ private void saveRecording() {
+ commitCurrentLogUnit();
+ publishLogBuffer(mUserRecordingLogBuffer, mUserRecordingLog, true);
+ mUserRecordingLog.close(null);
+ mUserRecordingLog = null;
+ mUserRecordingLogBuffer = null;
+
+ if (mFeedbackDialogBundle != null) {
+ mFeedbackDialogBundle.putBoolean(FeedbackFragment.KEY_HAS_USER_RECORDING, true);
+ }
+ clearRecordingTimer();
}
// TODO: currently unreachable. Remove after being sure enable/disable is
@@ -539,40 +729,116 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
}
*/
- private static final String[] EVENTKEYS_FEEDBACK = {
- "UserTimestamp", "contents"
- };
- public void sendFeedback(final String feedbackContents, final boolean includeHistory) {
- if (mFeedbackLogBuffer == null) {
+ /**
+ * Get the name of the first allowed account on the device.
+ *
+ * Allowed accounts must be in the domain given by ALLOWED_ACCOUNT_DOMAIN.
+ *
+ * @return The user's account name.
+ */
+ public String getAccountName() {
+ if (sAccountType == null || sAccountType.isEmpty()) {
+ return null;
+ }
+ if (sAllowedAccountDomain == null || sAllowedAccountDomain.isEmpty()) {
+ return null;
+ }
+ final AccountManager manager = AccountManager.get(mLatinIME);
+ // Filter first by account type.
+ final Account[] accounts = manager.getAccountsByType(sAccountType);
+
+ for (final Account account : accounts) {
+ if (DEBUG) {
+ Log.d(TAG, account.name);
+ }
+ final String[] parts = account.name.split("@");
+ if (parts.length > 1 && parts[1].equals(sAllowedAccountDomain)) {
+ return parts[0];
+ }
+ }
+ return null;
+ }
+
+ private static final LogStatement LOGSTATEMENT_FEEDBACK =
+ new LogStatement("UserFeedback", false, false, "contents", "accountName", "recording");
+ public void sendFeedback(final String feedbackContents, final boolean includeHistory,
+ final boolean isIncludingAccountName, final boolean isIncludingRecording) {
+ if (mSavedFeedbackLogBuffer == null) {
return;
}
- if (includeHistory) {
- commitCurrentLogUnit();
- } else {
- mFeedbackLogBuffer.clear();
+ if (!includeHistory) {
+ mSavedFeedbackLogBuffer.clear();
+ }
+ String recording = "";
+ if (isIncludingRecording) {
+ // Try to read recording from recently written json file
+ if (mUserRecordingFile != null) {
+ FileChannel channel = null;
+ try {
+ channel = new FileInputStream(mUserRecordingFile).getChannel();
+ final MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0,
+ channel.size());
+ // Android's openFileOutput() creates the file, so we use Android's default
+ // Charset (UTF-8) here to read it.
+ recording = Charset.defaultCharset().decode(buffer).toString();
+ } catch (FileNotFoundException e) {
+ Log.e(TAG, "Could not find recording file", e);
+ } catch (IOException e) {
+ Log.e(TAG, "Error reading recording file", e);
+ } finally {
+ if (channel != null) {
+ try {
+ channel.close();
+ } catch (IOException e) {
+ Log.e(TAG, "Error closing recording file", e);
+ }
+ }
+ }
+ }
}
final LogUnit feedbackLogUnit = new LogUnit();
- final Object[] values = {
- feedbackContents
- };
- feedbackLogUnit.addLogStatement(EVENTKEYS_FEEDBACK, values,
- false /* isPotentiallyPrivate */);
+ final String accountName = isIncludingAccountName ? getAccountName() : "";
+ feedbackLogUnit.addLogStatement(LOGSTATEMENT_FEEDBACK, SystemClock.uptimeMillis(),
+ feedbackContents, accountName, recording);
mFeedbackLogBuffer.shiftIn(feedbackLogUnit);
- publishLogBuffer(mFeedbackLogBuffer, mFeedbackLog, true /* isIncludingPrivateData */);
- mFeedbackLog.close(new Runnable() {
+ publishLogBuffer(mFeedbackLogBuffer, mSavedFeedbackLog, true /* isIncludingPrivateData */);
+ mSavedFeedbackLog.close(new Runnable() {
@Override
public void run() {
uploadNow();
}
});
- mFeedbackLog = new ResearchLog(createLogFile(mFilesDir));
+
+ if (isIncludingRecording && DEBUG_REPLAY_AFTER_FEEDBACK) {
+ final Handler handler = new Handler();
+ handler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ final ReplayData replayData =
+ mMotionEventReader.readMotionEventData(mUserRecordingFile);
+ mReplayer.replay(replayData, null);
+ }
+ }, 1000);
+ }
+
+ if (FEEDBACK_DIALOG_SHOULD_PRESERVE_TEXT_FIELD) {
+ // Use feedback string as a channel name to label feedback strings. Here we record the
+ // string for prepopulating the field next time.
+ final String channelName = feedbackContents;
+ if (mPrefs == null) {
+ return;
+ }
+ final Editor e = mPrefs.edit();
+ e.putString(PREF_RESEARCH_SAVED_CHANNEL, channelName);
+ e.apply();
+ }
}
public void uploadNow() {
if (DEBUG) {
Log.d(TAG, "calling uploadNow()");
}
- mInputMethodService.startService(mUploadIntent);
+ mLatinIME.startService(mUploadNowIntent);
}
public void onLeavingSendFeedbackDialog() {
@@ -586,19 +852,20 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
}
}
+ private Dictionary getDictionary() {
+ if (mSuggest == null) {
+ return null;
+ }
+ return mSuggest.getMainDictionary();
+ }
+
private void setIsPasswordView(boolean isPasswordView) {
mIsPasswordView = isPasswordView;
}
private boolean isAllowedToLog() {
- if (DEBUG) {
- Log.d(TAG, "iatl: " +
- "mipw=" + mIsPasswordView +
- ", mils=" + mIsLoggingSuspended +
- ", sil=" + sIsLogging +
- ", mInFeedbackDialog=" + mInFeedbackDialog);
- }
- return !mIsPasswordView && !mIsLoggingSuspended && sIsLogging && !mInFeedbackDialog;
+ return !mIsPasswordView && !mIsLoggingSuspended && sIsLogging && !mInFeedbackDialog
+ && !isReplaying();
}
public void requestIndicatorRedraw() {
@@ -611,27 +878,41 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
mMainKeyboardView.invalidateAllKeys();
}
+ private boolean isReplaying() {
+ return mReplayer.isReplaying();
+ }
+
+ private int getIndicatorColor() {
+ if (isMakingUserRecording()) {
+ return Color.YELLOW;
+ }
+ if (isReplaying()) {
+ return Color.GREEN;
+ }
+ return Color.RED;
+ }
public void paintIndicator(KeyboardView view, Paint paint, Canvas canvas, int width,
int height) {
// TODO: Reimplement using a keyboard background image specific to the ResearchLogger
// and remove this method.
- // The check for MainKeyboardView ensures that a red border is only placed around
- // the main keyboard, not every keyboard.
- if (IS_SHOWING_INDICATOR && isAllowedToLog() && view instanceof MainKeyboardView) {
+ // The check for MainKeyboardView ensures that the indicator only decorates the main
+ // keyboard, not every keyboard.
+ if (IS_SHOWING_INDICATOR && (isAllowedToLog() || isReplaying())
+ && view instanceof MainKeyboardView) {
final int savedColor = paint.getColor();
- paint.setColor(Color.RED);
+ paint.setColor(getIndicatorColor());
final Style savedStyle = paint.getStyle();
paint.setStyle(Style.STROKE);
final float savedStrokeWidth = paint.getStrokeWidth();
if (IS_SHOWING_INDICATOR_CLEARLY) {
paint.setStrokeWidth(5);
- canvas.drawRect(0, 0, width, height, paint);
+ canvas.drawLine(0, 0, 0, height, paint);
+ canvas.drawLine(width, 0, width, height, paint);
} else {
- // Put a tiny red dot on the screen so a knowledgeable user can check whether
- // it is enabled. The dot is actually a zero-width, zero-height rectangle,
- // placed at the lower-right corner of the canvas, painted with a non-zero border
- // width.
+ // Put a tiny dot on the screen so a knowledgeable user can check whether it is
+ // enabled. The dot is actually a zero-width, zero-height rectangle, placed at the
+ // lower-right corner of the canvas, painted with a non-zero border width.
paint.setStrokeWidth(3);
canvas.drawRect(width, height, width, height, paint);
}
@@ -641,97 +922,191 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
}
}
- private static final Object[] EVENTKEYS_NULLVALUES = {};
-
/**
* Buffer a research log event, flagging it as privacy-sensitive.
- *
- * This 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.
- *
- * @param keys an array containing a descriptive name for the event, followed by the keys
- * @param values an array of values, either a String or Number. length should be one
- * less than the keys array
*/
- private synchronized void enqueuePotentiallyPrivateEvent(final String[] keys,
- final Object[] values) {
- assert values.length + 1 == keys.length;
- if (isAllowedToLog()) {
- mCurrentLogUnit.addLogStatement(keys, values, true /* isPotentiallyPrivate */);
+ private synchronized void enqueueEvent(final LogStatement logStatement,
+ final Object... values) {
+ enqueueEvent(mCurrentLogUnit, logStatement, values);
+ }
+
+ private synchronized void enqueueEvent(final LogUnit logUnit, final LogStatement logStatement,
+ final Object... values) {
+ assert values.length == logStatement.getKeys().length;
+ if (isAllowedToLog() && logUnit != null) {
+ final long time = SystemClock.uptimeMillis();
+ logUnit.addLogStatement(logStatement, time, values);
}
}
private void setCurrentLogUnitContainsDigitFlag() {
- mCurrentLogUnit.setContainsDigit();
+ mCurrentLogUnit.setMayContainDigit();
}
- /**
- * Buffer a research log event, flaggint it as not privacy-sensitive.
- *
- * This event contains no potentially private information. Even if the word that this event
- * is privacy-sensitive, this event can still safely be sent to the output log. The system
- * waits until the containing word is known so that this event can be written in the proper
- * temporal order with other events that may be privacy sensitive.
- *
- * @param keys an array containing a descriptive name for the event, followed by the keys
- * @param values an array of values, either a String or Number. length should be one
- * less than the keys array
- */
- private synchronized void enqueueEvent(final String[] keys, final Object[] values) {
- assert values.length + 1 == keys.length;
- if (isAllowedToLog()) {
- mCurrentLogUnit.addLogStatement(keys, values, false /* isPotentiallyPrivate */);
- }
+ private void setCurrentLogUnitContainsCorrection() {
+ mCurrentLogUnit.setContainsCorrection();
+ }
+
+ private void setCurrentLogUnitCorrectionType(final int correctionType) {
+ mCurrentLogUnit.setCorrectionType(correctionType);
}
/* package for test */ void commitCurrentLogUnit() {
if (DEBUG) {
- Log.d(TAG, "commitCurrentLogUnit");
+ Log.d(TAG, "commitCurrentLogUnit" + (mCurrentLogUnit.hasWord() ?
+ ": " + mCurrentLogUnit.getWord() : ""));
}
if (!mCurrentLogUnit.isEmpty()) {
if (mMainLogBuffer != null) {
mMainLogBuffer.shiftIn(mCurrentLogUnit);
- if (mMainLogBuffer.isSafeToLog() && mMainResearchLog != null) {
- publishLogBuffer(mMainLogBuffer, mMainResearchLog,
- true /* isIncludingPrivateData */);
- mMainLogBuffer.resetWordCounter();
- }
}
if (mFeedbackLogBuffer != null) {
mFeedbackLogBuffer.shiftIn(mCurrentLogUnit);
}
+ if (mUserRecordingLogBuffer != null) {
+ mUserRecordingLogBuffer.shiftIn(mCurrentLogUnit);
+ }
+ mCurrentLogUnit = new LogUnit();
+ } else {
+ if (DEBUG) {
+ Log.d(TAG, "Warning: tried to commit empty log unit.");
+ }
+ }
+ }
+
+ private static final LogStatement LOGSTATEMENT_UNCOMMIT_CURRENT_LOGUNIT =
+ new LogStatement("UncommitCurrentLogUnit", false, false);
+ public void uncommitCurrentLogUnit(final String expectedWord,
+ final boolean dumpCurrentLogUnit) {
+ // The user has deleted this word and returned to the previous. Check that the word in the
+ // logUnit matches the expected word. If so, restore the last log unit committed to be the
+ // current logUnit. I.e., pull out the last LogUnit from all the LogBuffers, and make
+ // restore it to mCurrentLogUnit so the new edits are captured with the word. Optionally
+ // dump the contents of mCurrentLogUnit (useful if they contain deletions of the next word
+ // that should not be reported to protect user privacy)
+ //
+ // Note that we don't use mLastLogUnit here, because it only goes one word back and is only
+ // needed for reverts, which only happen one back.
+ if (mMainLogBuffer == null) {
+ return;
+ }
+ final LogUnit oldLogUnit = mMainLogBuffer.peekLastLogUnit();
+
+ // Check that expected word matches.
+ if (oldLogUnit != null) {
+ final String oldLogUnitWord = oldLogUnit.getWord();
+ if (!oldLogUnitWord.equals(expectedWord)) {
+ return;
+ }
+ }
+
+ // Uncommit, merging if necessary.
+ mMainLogBuffer.unshiftIn();
+ if (oldLogUnit != null && !dumpCurrentLogUnit) {
+ oldLogUnit.append(mCurrentLogUnit);
+ mSavedDownEventTime = Long.MAX_VALUE;
+ }
+ if (oldLogUnit == null) {
mCurrentLogUnit = new LogUnit();
- Log.d(TAG, "commitCurrentLogUnit");
+ } else {
+ mCurrentLogUnit = oldLogUnit;
+ }
+ if (mFeedbackLogBuffer != null) {
+ mFeedbackLogBuffer.unshiftIn();
+ }
+ enqueueEvent(LOGSTATEMENT_UNCOMMIT_CURRENT_LOGUNIT);
+ if (DEBUG) {
+ Log.d(TAG, "uncommitCurrentLogUnit (dump=" + dumpCurrentLogUnit + ") back to "
+ + (mCurrentLogUnit.hasWord() ? ": '" + mCurrentLogUnit.getWord() + "'" : ""));
}
}
+ /**
+ * Publish all the logUnits in the logBuffer, without doing any privacy filtering.
+ */
/* package for test */ void publishLogBuffer(final LogBuffer logBuffer,
- final ResearchLog researchLog, final boolean isIncludingPrivateData) {
- LogUnit logUnit;
- while ((logUnit = logBuffer.shiftOut()) != null) {
- researchLog.publish(logUnit, isIncludingPrivateData);
+ final ResearchLog researchLog, final boolean canIncludePrivateData) {
+ publishLogUnits(logBuffer.getLogUnits(), researchLog, canIncludePrivateData);
+ }
+
+ private static final LogStatement LOGSTATEMENT_LOG_SEGMENT_OPENING =
+ new LogStatement("logSegmentStart", false, false, "isIncludingPrivateData");
+ private static final LogStatement LOGSTATEMENT_LOG_SEGMENT_CLOSING =
+ new LogStatement("logSegmentEnd", false, false);
+ /**
+ * Publish all LogUnits in a list.
+ *
+ * Any privacy checks should be performed before calling this method.
+ */
+ /* package for test */ void publishLogUnits(final List<LogUnit> logUnits,
+ final ResearchLog researchLog, final boolean canIncludePrivateData) {
+ final LogUnit openingLogUnit = new LogUnit();
+ if (logUnits.isEmpty()) return;
+ // LogUnits not containing private data, such as contextual data for the log, do not require
+ // logSegment boundary statements.
+ if (canIncludePrivateData) {
+ openingLogUnit.addLogStatement(LOGSTATEMENT_LOG_SEGMENT_OPENING,
+ SystemClock.uptimeMillis(), canIncludePrivateData);
+ researchLog.publish(openingLogUnit, true /* isIncludingPrivateData */);
+ }
+ for (LogUnit logUnit : logUnits) {
+ if (DEBUG) {
+ Log.d(TAG, "publishLogBuffer: " + (logUnit.hasWord() ? logUnit.getWord()
+ : "<wordless>") + ", correction?: " + logUnit.containsCorrection());
+ }
+ researchLog.publish(logUnit, canIncludePrivateData);
+ }
+ if (canIncludePrivateData) {
+ final LogUnit closingLogUnit = new LogUnit();
+ closingLogUnit.addLogStatement(LOGSTATEMENT_LOG_SEGMENT_CLOSING,
+ SystemClock.uptimeMillis());
+ researchLog.publish(closingLogUnit, true /* isIncludingPrivateData */);
}
}
- private boolean hasOnlyLetters(final String word) {
+ public static boolean hasLetters(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;
+ if (Character.isLetter(codePoint)) {
+ return true;
}
}
- return true;
+ return false;
}
- private void onWordComplete(final String word) {
- Log.d(TAG, "onWordComplete: " + word);
- if (word != null && word.length() > 0 && hasOnlyLetters(word)) {
+ /**
+ * Commit the portion of mCurrentLogUnit before maxTime as a worded logUnit.
+ *
+ * After this operation completes, mCurrentLogUnit will hold any logStatements that happened
+ * after maxTime.
+ */
+ /* package for test */ void commitCurrentLogUnitAsWord(final String word, final long maxTime,
+ final boolean isBatchMode) {
+ if (word == null) {
+ return;
+ }
+ if (word.length() > 0 && hasLetters(word)) {
mCurrentLogUnit.setWord(word);
- mStatistics.recordWordEntered();
}
+ final LogUnit newLogUnit = mCurrentLogUnit.splitByTime(maxTime);
+ enqueueCommitText(word, isBatchMode);
commitCurrentLogUnit();
+ mCurrentLogUnit = newLogUnit;
+ }
+
+ /**
+ * Record the time of a MotionEvent.ACTION_DOWN.
+ *
+ * Warning: Not thread safe. Only call from the main thread.
+ */
+ private void setSavedDownEventTime(final long time) {
+ mSavedDownEventTime = time;
+ }
+
+ public void onWordFinished(final String word, final boolean isBatchMode) {
+ commitCurrentLogUnitAsWord(word, mSavedDownEventTime, isBatchMode);
+ mSavedDownEventTime = Long.MAX_VALUE;
}
private static int scrubDigitFromCodePoint(int codePoint) {
@@ -775,120 +1150,141 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
}
private String scrubWord(String word) {
- if (mDictionary == null) {
+ final Dictionary dictionary = getDictionary();
+ if (dictionary == null) {
return WORD_REPLACEMENT_STRING;
}
- if (mDictionary.isValidWord(word)) {
+ if (dictionary.isValidWord(word)) {
return word;
}
return WORD_REPLACEMENT_STRING;
}
- private static final String[] EVENTKEYS_LATINIME_ONSTARTINPUTVIEWINTERNAL = {
- "LatinIMEOnStartInputViewInternal", "uuid", "packageName", "inputType", "imeOptions",
- "fieldId", "display", "model", "prefs", "versionCode", "versionName", "outputFormatVersion"
- };
+ // Specific logging methods follow below. The comments for each logging method should
+ // indicate what specific method is logged, and how to trigger it from the user interface.
+ //
+ // Logging methods can be generally classified into two flavors, "UserAction", which should
+ // correspond closely to an event that is sensed by the IME, and is usually generated
+ // directly by the user, and "SystemResponse" which corresponds to an event that the IME
+ // generates, often after much processing of user input. SystemResponses should correspond
+ // closely to user-visible events.
+ // TODO: Consider exposing the UserAction classification in the log output.
+
+ /**
+ * Log a call to LatinIME.onStartInputViewInternal().
+ *
+ * UserAction: called each time the keyboard is opened up.
+ */
+ private static final LogStatement LOGSTATEMENT_LATIN_IME_ON_START_INPUT_VIEW_INTERNAL =
+ new LogStatement("LatinImeOnStartInputViewInternal", false, false, "uuid",
+ "packageName", "inputType", "imeOptions", "fieldId", "display", "model",
+ "prefs", "versionCode", "versionName", "outputFormatVersion", "logEverything",
+ "isExperimentalDebug");
public static void latinIME_onStartInputViewInternal(final EditorInfo editorInfo,
final SharedPreferences prefs) {
final ResearchLogger researchLogger = getInstance();
- researchLogger.start();
if (editorInfo != null) {
- final Context context = researchLogger.mInputMethodService;
+ final boolean isPassword = InputTypeUtils.isPasswordInputType(editorInfo.inputType)
+ || InputTypeUtils.isVisiblePasswordInputType(editorInfo.inputType);
+ getInstance().setIsPasswordView(isPassword);
+ researchLogger.start();
+ final Context context = researchLogger.mLatinIME;
try {
final PackageInfo packageInfo;
packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(),
0);
final Integer versionCode = packageInfo.versionCode;
final String versionName = packageInfo.versionName;
- final Object[] values = {
+ researchLogger.enqueueEvent(LOGSTATEMENT_LATIN_IME_ON_START_INPUT_VIEW_INTERNAL,
researchLogger.mUUIDString, editorInfo.packageName,
Integer.toHexString(editorInfo.inputType),
Integer.toHexString(editorInfo.imeOptions), editorInfo.fieldId,
Build.DISPLAY, Build.MODEL, prefs, versionCode, versionName,
- OUTPUT_FORMAT_VERSION
- };
- researchLogger.enqueueEvent(EVENTKEYS_LATINIME_ONSTARTINPUTVIEWINTERNAL, values);
+ OUTPUT_FORMAT_VERSION, IS_LOGGING_EVERYTHING,
+ ProductionFlag.IS_EXPERIMENTAL_DEBUG);
} catch (NameNotFoundException e) {
e.printStackTrace();
}
}
}
- public void latinIME_onFinishInputInternal() {
+ public void latinIME_onFinishInputViewInternal() {
stop();
}
- private static final String[] EVENTKEYS_USER_FEEDBACK = {
- "UserFeedback", "FeedbackContents"
- };
-
- private static final String[] EVENTKEYS_PREFS_CHANGED = {
- "PrefsChanged", "prefs"
- };
+ /**
+ * Log a change in preferences.
+ *
+ * UserAction: called when the user changes the settings.
+ */
+ private static final LogStatement LOGSTATEMENT_PREFS_CHANGED =
+ new LogStatement("PrefsChanged", false, false, "prefs");
public static void prefsChanged(final SharedPreferences prefs) {
final ResearchLogger researchLogger = getInstance();
- final Object[] values = {
- prefs
- };
- researchLogger.enqueueEvent(EVENTKEYS_PREFS_CHANGED, values);
+ researchLogger.enqueueEvent(LOGSTATEMENT_PREFS_CHANGED, prefs);
}
- // Regular logging methods
-
- private static final String[] EVENTKEYS_MAINKEYBOARDVIEW_PROCESSMOTIONEVENT = {
- "MainKeyboardViewProcessMotionEvent", "action", "eventTime", "id", "x", "y", "size",
- "pressure"
- };
+ /**
+ * Log a call to MainKeyboardView.processMotionEvent().
+ *
+ * UserAction: called when the user puts their finger onto the screen (ACTION_DOWN).
+ *
+ */
+ private static final LogStatement LOGSTATEMENT_MAIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENT =
+ new LogStatement("MotionEvent", true, false, "action",
+ LogStatement.KEY_IS_LOGGING_RELATED, "motionEvent");
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) {
- final String actionString;
- switch (action) {
- case MotionEvent.ACTION_CANCEL: actionString = "CANCEL"; break;
- case MotionEvent.ACTION_UP: actionString = "UP"; break;
- case MotionEvent.ACTION_DOWN: actionString = "DOWN"; break;
- case MotionEvent.ACTION_POINTER_UP: actionString = "POINTER_UP"; break;
- case MotionEvent.ACTION_POINTER_DOWN: actionString = "POINTER_DOWN"; break;
- case MotionEvent.ACTION_MOVE: actionString = "MOVE"; break;
- case MotionEvent.ACTION_OUTSIDE: actionString = "OUTSIDE"; break;
- default: actionString = "ACTION_" + action; break;
+ final String actionString = LoggingUtils.getMotionEventActionTypeString(action);
+ final ResearchLogger researchLogger = getInstance();
+ researchLogger.enqueueEvent(LOGSTATEMENT_MAIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENT,
+ actionString, false /* IS_LOGGING_RELATED */, MotionEvent.obtain(me));
+ if (action == MotionEvent.ACTION_DOWN) {
+ // Subtract 1 from eventTime so the down event is included in the later
+ // LogUnit, not the earlier (the test is for inequality).
+ researchLogger.setSavedDownEventTime(eventTime - 1);
+ }
+ // Refresh the timer in case we are capturing user feedback.
+ if (researchLogger.isMakingUserRecording()) {
+ researchLogger.resetRecordingTimer();
}
- final float size = me.getSize(index);
- final float pressure = me.getPressure(index);
- final Object[] values = {
- actionString, eventTime, id, x, y, size, pressure
- };
- getInstance().enqueuePotentiallyPrivateEvent(
- EVENTKEYS_MAINKEYBOARDVIEW_PROCESSMOTIONEVENT, values);
}
}
- private static final String[] EVENTKEYS_LATINIME_ONCODEINPUT = {
- "LatinIMEOnCodeInput", "code", "x", "y"
- };
+ /**
+ * Log a call to LatinIME.onCodeInput().
+ *
+ * SystemResponse: The main processing step for entering text. Called when the user performs a
+ * tap, a flick, a long press, releases a gesture, or taps a punctuation suggestion.
+ */
+ private static final LogStatement LOGSTATEMENT_LATIN_IME_ON_CODE_INPUT =
+ new LogStatement("LatinImeOnCodeInput", true, false, "code", "x", "y");
public static void latinIME_onCodeInput(final int code, final int x, final int y) {
final long time = SystemClock.uptimeMillis();
final ResearchLogger researchLogger = getInstance();
- final Object[] values = {
- Keyboard.printableCode(scrubDigitFromCodePoint(code)), x, y
- };
- researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_ONCODEINPUT, values);
+ researchLogger.enqueueEvent(LOGSTATEMENT_LATIN_IME_ON_CODE_INPUT,
+ Constants.printableCode(scrubDigitFromCodePoint(code)), x, y);
if (Character.isDigit(code)) {
researchLogger.setCurrentLogUnitContainsDigitFlag();
}
researchLogger.mStatistics.recordChar(code, time);
}
-
- private static final String[] EVENTKEYS_LATINIME_ONDISPLAYCOMPLETIONS = {
- "LatinIMEOnDisplayCompletions", "applicationSpecifiedCompletions"
- };
+ /**
+ * Log a call to LatinIME.onDisplayCompletions().
+ *
+ * SystemResponse: The IME has displayed application-specific completions. They may show up
+ * in the suggestion strip, such as a landscape phone.
+ */
+ private static final LogStatement LOGSTATEMENT_LATINIME_ONDISPLAYCOMPLETIONS =
+ new LogStatement("LatinIMEOnDisplayCompletions", true, true,
+ "applicationSpecifiedCompletions");
public static void latinIME_onDisplayCompletions(
final CompletionInfo[] applicationSpecifiedCompletions) {
- final Object[] values = {
- applicationSpecifiedCompletions
- };
- getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_ONDISPLAYCOMPLETIONS,
- values);
+ // Note; passing an array as a single element in a vararg list. Must create a new
+ // dummy array around it or it will get expanded.
+ getInstance().enqueueEvent(LOGSTATEMENT_LATINIME_ONDISPLAYCOMPLETIONS,
+ new Object[] { applicationSpecifiedCompletions });
}
public static boolean getAndClearLatinIMEExpectingUpdateSelection() {
@@ -897,27 +1293,35 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
return returnValue;
}
- private static final String[] EVENTKEYS_LATINIME_ONWINDOWHIDDEN = {
- "LatinIMEOnWindowHidden", "isTextTruncated", "text"
- };
+ /**
+ * Log a call to LatinIME.onWindowHidden().
+ *
+ * UserAction: The user has performed an action that has caused the IME to be closed. They may
+ * have focused on something other than a text field, or explicitly closed it.
+ */
+ private static final LogStatement LOGSTATEMENT_LATINIME_ONWINDOWHIDDEN =
+ new LogStatement("LatinIMEOnWindowHidden", false, false, "isTextTruncated", "text");
public static void latinIME_onWindowHidden(final int savedSelectionStart,
final int savedSelectionEnd, final InputConnection ic) {
if (ic != null) {
- // Capture the TextView contents. This will trigger onUpdateSelection(), so we
- // set sLatinIMEExpectingUpdateSelection so that when onUpdateSelection() is called,
- // it can tell that it was generated by the logging code, and not by the user, and
- // therefore keep user-visible state as is.
- ic.beginBatchEdit();
- ic.performContextMenuAction(android.R.id.selectAll);
- CharSequence charSequence = ic.getSelectedText(0);
- ic.setSelection(savedSelectionStart, savedSelectionEnd);
- ic.endBatchEdit();
- sLatinIMEExpectingUpdateSelection = true;
- final Object[] values = new Object[2];
- if (OUTPUT_ENTIRE_BUFFER) {
+ final boolean isTextTruncated;
+ final String text;
+ if (LOG_FULL_TEXTVIEW_CONTENTS) {
+ // Capture the TextView contents. This will trigger onUpdateSelection(), so we
+ // set sLatinIMEExpectingUpdateSelection so that when onUpdateSelection() is called,
+ // it can tell that it was generated by the logging code, and not by the user, and
+ // therefore keep user-visible state as is.
+ ic.beginBatchEdit();
+ ic.performContextMenuAction(android.R.id.selectAll);
+ CharSequence charSequence = ic.getSelectedText(0);
+ if (savedSelectionStart != -1 && savedSelectionEnd != -1) {
+ ic.setSelection(savedSelectionStart, savedSelectionEnd);
+ }
+ ic.endBatchEdit();
+ sLatinIMEExpectingUpdateSelection = true;
if (TextUtils.isEmpty(charSequence)) {
- values[0] = false;
- values[1] = "";
+ isTextTruncated = false;
+ text = "";
} else {
if (charSequence.length() > MAX_INPUTVIEW_LENGTH_TO_CAPTURE) {
int length = MAX_INPUTVIEW_LENGTH_TO_CAPTURE;
@@ -928,29 +1332,39 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
}
final CharSequence truncatedCharSequence = charSequence.subSequence(0,
length);
- values[0] = true;
- values[1] = truncatedCharSequence.toString();
+ isTextTruncated = true;
+ text = truncatedCharSequence.toString();
} else {
- values[0] = false;
- values[1] = charSequence.toString();
+ isTextTruncated = false;
+ text = charSequence.toString();
}
}
} else {
- values[0] = true;
- values[1] = "";
+ isTextTruncated = true;
+ text = "";
}
final ResearchLogger researchLogger = getInstance();
- researchLogger.enqueueEvent(EVENTKEYS_LATINIME_ONWINDOWHIDDEN, values);
+ // Assume that OUTPUT_ENTIRE_BUFFER is only true when we don't care about privacy (e.g.
+ // during a live user test), so the normal isPotentiallyPrivate and
+ // isPotentiallyRevealing flags do not apply
+ researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_ONWINDOWHIDDEN, isTextTruncated,
+ text);
researchLogger.commitCurrentLogUnit();
getInstance().stop();
}
}
- private static final String[] EVENTKEYS_LATINIME_ONUPDATESELECTION = {
- "LatinIMEOnUpdateSelection", "lastSelectionStart", "lastSelectionEnd", "oldSelStart",
- "oldSelEnd", "newSelStart", "newSelEnd", "composingSpanStart", "composingSpanEnd",
- "expectingUpdateSelection", "expectingUpdateSelectionFromLogger", "context"
- };
+ /**
+ * Log a call to LatinIME.onUpdateSelection().
+ *
+ * UserAction/SystemResponse: The user has moved the cursor or selection. This function may
+ * be called, however, when the system has moved the cursor, say by inserting a character.
+ */
+ private static final LogStatement LOGSTATEMENT_LATINIME_ONUPDATESELECTION =
+ new LogStatement("LatinIMEOnUpdateSelection", true, false, "lastSelectionStart",
+ "lastSelectionEnd", "oldSelStart", "oldSelEnd", "newSelStart", "newSelEnd",
+ "composingSpanStart", "composingSpanEnd", "expectingUpdateSelection",
+ "expectingUpdateSelectionFromLogger", "context");
public static void latinIME_onUpdateSelection(final int lastSelectionStart,
final int lastSelectionEnd, final int oldSelStart, final int oldSelEnd,
final int newSelStart, final int newSelEnd, final int composingSpanStart,
@@ -966,350 +1380,649 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
}
final ResearchLogger researchLogger = getInstance();
final String scrubbedWord = researchLogger.scrubWord(word);
- final Object[] values = {
- lastSelectionStart, lastSelectionEnd, oldSelStart, oldSelEnd, newSelStart,
- newSelEnd, composingSpanStart, composingSpanEnd, expectingUpdateSelection,
- expectingUpdateSelectionFromLogger, scrubbedWord
- };
- researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_ONUPDATESELECTION, values);
+ researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_ONUPDATESELECTION, lastSelectionStart,
+ lastSelectionEnd, oldSelStart, oldSelEnd, newSelStart, newSelEnd,
+ composingSpanStart, composingSpanEnd, expectingUpdateSelection,
+ expectingUpdateSelectionFromLogger, scrubbedWord);
}
- private static final String[] EVENTKEYS_LATINIME_PICKSUGGESTIONMANUALLY = {
- "LatinIMEPickSuggestionManually", "replacedWord", "index", "suggestion", "x", "y"
- };
+ /**
+ * Log a call to LatinIME.onTextInput().
+ *
+ * SystemResponse: Raw text is added to the TextView.
+ */
+ public static void latinIME_onTextInput(final String text, final boolean isBatchMode) {
+ final ResearchLogger researchLogger = getInstance();
+ researchLogger.commitCurrentLogUnitAsWord(text, Long.MAX_VALUE, isBatchMode);
+ }
+
+ /**
+ * Log a call to LatinIME.pickSuggestionManually().
+ *
+ * UserAction: The user has chosen a specific word from the suggestion strip.
+ */
+ private static final LogStatement LOGSTATEMENT_LATINIME_PICKSUGGESTIONMANUALLY =
+ new LogStatement("LatinIMEPickSuggestionManually", true, false, "replacedWord", "index",
+ "suggestion", "x", "y", "isBatchMode");
public static void latinIME_pickSuggestionManually(final String replacedWord,
- final int index, CharSequence suggestion) {
- final Object[] values = {
- scrubDigitsFromString(replacedWord), index,
- (suggestion == null ? null : scrubDigitsFromString(suggestion.toString())),
- Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE
- };
+ final int index, final String suggestion, final boolean isBatchMode) {
final ResearchLogger researchLogger = getInstance();
- researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_PICKSUGGESTIONMANUALLY,
- values);
+ if (!replacedWord.equals(suggestion.toString())) {
+ // The user chose something other than what was already there.
+ researchLogger.setCurrentLogUnitContainsCorrection();
+ researchLogger.setCurrentLogUnitCorrectionType(LogUnit.CORRECTIONTYPE_TYPO);
+ }
+ final String scrubbedWord = scrubDigitsFromString(suggestion);
+ researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_PICKSUGGESTIONMANUALLY,
+ scrubDigitsFromString(replacedWord), index,
+ suggestion == null ? null : scrubbedWord, Constants.SUGGESTION_STRIP_COORDINATE,
+ Constants.SUGGESTION_STRIP_COORDINATE, isBatchMode);
+ researchLogger.commitCurrentLogUnitAsWord(scrubbedWord, Long.MAX_VALUE, isBatchMode);
+ researchLogger.mStatistics.recordManualSuggestion(SystemClock.uptimeMillis());
}
- private static final String[] EVENTKEYS_LATINIME_PUNCTUATIONSUGGESTION = {
- "LatinIMEPunctuationSuggestion", "index", "suggestion", "x", "y"
- };
- public static void latinIME_punctuationSuggestion(final int index,
- final CharSequence suggestion) {
- final Object[] values = {
- index, suggestion,
- Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE
- };
- getInstance().enqueueEvent(EVENTKEYS_LATINIME_PUNCTUATIONSUGGESTION, values);
+ /**
+ * Log a call to LatinIME.punctuationSuggestion().
+ *
+ * UserAction: The user has chosen punctuation from the punctuation suggestion strip.
+ */
+ private static final LogStatement LOGSTATEMENT_LATINIME_PUNCTUATIONSUGGESTION =
+ new LogStatement("LatinIMEPunctuationSuggestion", false, false, "index", "suggestion",
+ "x", "y", "isPrediction");
+ public static void latinIME_punctuationSuggestion(final int index, final String suggestion,
+ final boolean isBatchMode, final boolean isPrediction) {
+ final ResearchLogger researchLogger = getInstance();
+ researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_PUNCTUATIONSUGGESTION, index, suggestion,
+ Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE,
+ isPrediction);
+ researchLogger.commitCurrentLogUnitAsWord(suggestion, Long.MAX_VALUE, isBatchMode);
}
- private static final String[] EVENTKEYS_LATINIME_SENDKEYCODEPOINT = {
- "LatinIMESendKeyCodePoint", "code"
- };
+ /**
+ * Log a call to LatinIME.sendKeyCodePoint().
+ *
+ * SystemResponse: The IME is inserting text into the TextView for numbers, fixed strings, or
+ * some other unusual mechanism.
+ */
+ private static final LogStatement LOGSTATEMENT_LATINIME_SENDKEYCODEPOINT =
+ new LogStatement("LatinIMESendKeyCodePoint", true, false, "code");
public static void latinIME_sendKeyCodePoint(final int code) {
- final Object[] values = {
- Keyboard.printableCode(scrubDigitFromCodePoint(code))
- };
final ResearchLogger researchLogger = getInstance();
- researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_SENDKEYCODEPOINT, values);
+ researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_SENDKEYCODEPOINT,
+ Constants.printableCode(scrubDigitFromCodePoint(code)));
if (Character.isDigit(code)) {
researchLogger.setCurrentLogUnitContainsDigitFlag();
}
}
- private static final String[] EVENTKEYS_LATINIME_SWAPSWAPPERANDSPACE = {
- "LatinIMESwapSwapperAndSpace"
- };
- public static void latinIME_swapSwapperAndSpace() {
- getInstance().enqueueEvent(EVENTKEYS_LATINIME_SWAPSWAPPERANDSPACE, EVENTKEYS_NULLVALUES);
+ /**
+ * Log a call to LatinIME.promotePhantomSpace().
+ *
+ * SystemResponse: The IME is inserting a real space in place of a phantom space.
+ */
+ private static final LogStatement LOGSTATEMENT_LATINIME_PROMOTEPHANTOMSPACE =
+ new LogStatement("LatinIMEPromotPhantomSpace", false, false);
+ public static void latinIME_promotePhantomSpace() {
+ final ResearchLogger researchLogger = getInstance();
+ final LogUnit logUnit;
+ if (researchLogger.mMainLogBuffer == null) {
+ logUnit = researchLogger.mCurrentLogUnit;
+ } else {
+ logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit();
+ }
+ researchLogger.enqueueEvent(logUnit, LOGSTATEMENT_LATINIME_PROMOTEPHANTOMSPACE);
}
- private static final String[] EVENTKEYS_MAINKEYBOARDVIEW_ONLONGPRESS = {
- "MainKeyboardViewOnLongPress"
- };
+ /**
+ * Log a call to LatinIME.swapSwapperAndSpace().
+ *
+ * SystemResponse: A symbol has been swapped with a space character. E.g. punctuation may swap
+ * if a soft space is inserted after a word.
+ */
+ private static final LogStatement LOGSTATEMENT_LATINIME_SWAPSWAPPERANDSPACE =
+ new LogStatement("LatinIMESwapSwapperAndSpace", false, false, "originalCharacters",
+ "charactersAfterSwap");
+ public static void latinIME_swapSwapperAndSpace(final CharSequence originalCharacters,
+ final String charactersAfterSwap) {
+ final ResearchLogger researchLogger = getInstance();
+ final LogUnit logUnit;
+ if (researchLogger.mMainLogBuffer == null) {
+ logUnit = null;
+ } else {
+ logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit();
+ }
+ if (logUnit != null) {
+ researchLogger.enqueueEvent(logUnit, LOGSTATEMENT_LATINIME_SWAPSWAPPERANDSPACE,
+ originalCharacters, charactersAfterSwap);
+ }
+ }
+
+ /**
+ * Log a call to LatinIME.maybeDoubleSpacePeriod().
+ *
+ * SystemResponse: Two spaces have been replaced by period space.
+ */
+ public static void latinIME_maybeDoubleSpacePeriod(final String text,
+ final boolean isBatchMode) {
+ final ResearchLogger researchLogger = getInstance();
+ researchLogger.commitCurrentLogUnitAsWord(text, Long.MAX_VALUE, isBatchMode);
+ }
+
+ /**
+ * Log a call to MainKeyboardView.onLongPress().
+ *
+ * UserAction: The user has performed a long-press on a key.
+ */
+ private static final LogStatement LOGSTATEMENT_MAINKEYBOARDVIEW_ONLONGPRESS =
+ new LogStatement("MainKeyboardViewOnLongPress", false, false);
public static void mainKeyboardView_onLongPress() {
- getInstance().enqueueEvent(EVENTKEYS_MAINKEYBOARDVIEW_ONLONGPRESS, EVENTKEYS_NULLVALUES);
+ getInstance().enqueueEvent(LOGSTATEMENT_MAINKEYBOARDVIEW_ONLONGPRESS);
}
- private static final String[] EVENTKEYS_MAINKEYBOARDVIEW_SETKEYBOARD = {
- "MainKeyboardViewSetKeyboard", "elementId", "locale", "orientation", "width",
- "modeName", "action", "navigateNext", "navigatePrevious", "clobberSettingsKey",
- "passwordInput", "shortcutKeyEnabled", "hasShortcutKey", "languageSwitchKeyEnabled",
- "isMultiLine", "tw", "th", "keys"
- };
+ /**
+ * Log a call to MainKeyboardView.setKeyboard().
+ *
+ * SystemResponse: The IME has switched to a new keyboard (e.g. French, English).
+ * This is typically called right after LatinIME.onStartInputViewInternal (when starting a new
+ * IME), but may happen at other times if the user explicitly requests a keyboard change.
+ */
+ private static final LogStatement LOGSTATEMENT_MAINKEYBOARDVIEW_SETKEYBOARD =
+ new LogStatement("MainKeyboardViewSetKeyboard", false, false, "elementId", "locale",
+ "orientation", "width", "modeName", "action", "navigateNext",
+ "navigatePrevious", "clobberSettingsKey", "passwordInput", "shortcutKeyEnabled",
+ "hasShortcutKey", "languageSwitchKeyEnabled", "isMultiLine", "tw", "th",
+ "keys");
public static void mainKeyboardView_setKeyboard(final Keyboard keyboard) {
- if (keyboard != null) {
- final KeyboardId kid = keyboard.mId;
- final boolean isPasswordView = kid.passwordInput();
- getInstance().setIsPasswordView(isPasswordView);
- final Object[] values = {
+ final KeyboardId kid = keyboard.mId;
+ final boolean isPasswordView = kid.passwordInput();
+ final ResearchLogger researchLogger = getInstance();
+ researchLogger.setIsPasswordView(isPasswordView);
+ researchLogger.enqueueEvent(LOGSTATEMENT_MAINKEYBOARDVIEW_SETKEYBOARD,
KeyboardId.elementIdToName(kid.mElementId),
kid.mLocale + ":" + kid.mSubtype.getExtraValueOf(KEYBOARD_LAYOUT_SET),
- kid.mOrientation,
- kid.mWidth,
- KeyboardId.modeName(kid.mMode),
- kid.imeAction(),
- kid.navigateNext(),
- kid.navigatePrevious(),
- kid.mClobberSettingsKey,
- isPasswordView,
- kid.mShortcutKeyEnabled,
- kid.mHasShortcutKey,
- kid.mLanguageSwitchKeyEnabled,
- kid.isMultiLine(),
- keyboard.mOccupiedWidth,
- keyboard.mOccupiedHeight,
- keyboard.mKeys
- };
- getInstance().setIsPasswordView(isPasswordView);
- getInstance().enqueueEvent(EVENTKEYS_MAINKEYBOARDVIEW_SETKEYBOARD, values);
- }
+ kid.mOrientation, kid.mWidth, KeyboardId.modeName(kid.mMode), kid.imeAction(),
+ kid.navigateNext(), kid.navigatePrevious(), kid.mClobberSettingsKey,
+ isPasswordView, kid.mShortcutKeyEnabled, kid.mHasShortcutKey,
+ kid.mLanguageSwitchKeyEnabled, kid.isMultiLine(), keyboard.mOccupiedWidth,
+ keyboard.mOccupiedHeight, keyboard.mKeys);
}
- private static final String[] EVENTKEYS_LATINIME_REVERTCOMMIT = {
- "LatinIMERevertCommit", "originallyTypedWord"
- };
- public static void latinIME_revertCommit(final String originallyTypedWord) {
- final Object[] values = {
- originallyTypedWord
- };
- getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_REVERTCOMMIT, values);
+ /**
+ * Log a call to LatinIME.revertCommit().
+ *
+ * SystemResponse: The IME has reverted commited text. This happens when the user enters
+ * a word, commits it by pressing space or punctuation, and then reverts the commit by hitting
+ * backspace.
+ */
+ private static final LogStatement LOGSTATEMENT_LATINIME_REVERTCOMMIT =
+ new LogStatement("LatinIMERevertCommit", true, false, "committedWord",
+ "originallyTypedWord", "separatorString");
+ public static void latinIME_revertCommit(final String committedWord,
+ final String originallyTypedWord, final boolean isBatchMode,
+ final String separatorString) {
+ final ResearchLogger researchLogger = getInstance();
+ // TODO: Verify that mCurrentLogUnit has been restored and contains the reverted word.
+ final LogUnit logUnit;
+ if (researchLogger.mMainLogBuffer == null) {
+ logUnit = null;
+ } else {
+ logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit();
+ }
+ if (originallyTypedWord.length() > 0 && hasLetters(originallyTypedWord)) {
+ if (logUnit != null) {
+ logUnit.setWord(originallyTypedWord);
+ }
+ }
+ researchLogger.enqueueEvent(logUnit != null ? logUnit : researchLogger.mCurrentLogUnit,
+ LOGSTATEMENT_LATINIME_REVERTCOMMIT, committedWord, originallyTypedWord,
+ separatorString);
+ if (logUnit != null) {
+ logUnit.setContainsCorrection();
+ }
+ researchLogger.mStatistics.recordRevertCommit(SystemClock.uptimeMillis());
+ researchLogger.commitCurrentLogUnitAsWord(originallyTypedWord, Long.MAX_VALUE, isBatchMode);
}
- private static final String[] EVENTKEYS_POINTERTRACKER_CALLLISTENERONCANCELINPUT = {
- "PointerTrackerCallListenerOnCancelInput"
- };
+ /**
+ * Log a call to PointerTracker.callListenerOnCancelInput().
+ *
+ * UserAction: The user has canceled the input, e.g., by pressing down, but then removing
+ * outside the keyboard area.
+ * TODO: Verify
+ */
+ private static final LogStatement LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCANCELINPUT =
+ new LogStatement("PointerTrackerCallListenerOnCancelInput", false, false);
public static void pointerTracker_callListenerOnCancelInput() {
- getInstance().enqueueEvent(EVENTKEYS_POINTERTRACKER_CALLLISTENERONCANCELINPUT,
- EVENTKEYS_NULLVALUES);
+ getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCANCELINPUT);
}
- private static final String[] EVENTKEYS_POINTERTRACKER_CALLLISTENERONCODEINPUT = {
- "PointerTrackerCallListenerOnCodeInput", "code", "outputText", "x", "y",
- "ignoreModifierKey", "altersCode", "isEnabled"
- };
+ /**
+ * Log a call to PointerTracker.callListenerOnCodeInput().
+ *
+ * SystemResponse: The user has entered a key through the normal tapping mechanism.
+ * LatinIME.onCodeInput will also be called.
+ */
+ private static final LogStatement LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCODEINPUT =
+ new LogStatement("PointerTrackerCallListenerOnCodeInput", true, false, "code",
+ "outputText", "x", "y", "ignoreModifierKey", "altersCode", "isEnabled");
public static void pointerTracker_callListenerOnCodeInput(final Key key, final int x,
final int y, final boolean ignoreModifierKey, final boolean altersCode,
final int code) {
if (key != null) {
String outputText = key.getOutputText();
- final Object[] values = {
- Keyboard.printableCode(scrubDigitFromCodePoint(code)), outputText == null ? null
- : scrubDigitsFromString(outputText.toString()),
- x, y, ignoreModifierKey, altersCode, key.isEnabled()
- };
- getInstance().enqueuePotentiallyPrivateEvent(
- EVENTKEYS_POINTERTRACKER_CALLLISTENERONCODEINPUT, values);
+ 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 static final String[] EVENTKEYS_POINTERTRACKER_CALLLISTENERONRELEASE = {
- "PointerTrackerCallListenerOnRelease", "code", "withSliding", "ignoreModifierKey",
- "isEnabled"
- };
+ private void suppressResearchKeyMotionData() {
+ mCurrentLogUnit.removeResearchButtonInvocation();
+ }
+
+ /**
+ * Log a call to PointerTracker.callListenerCallListenerOnRelease().
+ *
+ * UserAction: The user has released their finger or thumb from the screen.
+ */
+ private static final LogStatement LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONRELEASE =
+ new LogStatement("PointerTrackerCallListenerOnRelease", true, false, "code",
+ "withSliding", "ignoreModifierKey", "isEnabled");
public static void pointerTracker_callListenerOnRelease(final Key key, final int primaryCode,
final boolean withSliding, final boolean ignoreModifierKey) {
if (key != null) {
- final Object[] values = {
- Keyboard.printableCode(scrubDigitFromCodePoint(primaryCode)), withSliding,
- ignoreModifierKey, key.isEnabled()
- };
- getInstance().enqueuePotentiallyPrivateEvent(
- EVENTKEYS_POINTERTRACKER_CALLLISTENERONRELEASE, values);
+ getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONRELEASE,
+ Constants.printableCode(scrubDigitFromCodePoint(primaryCode)), withSliding,
+ ignoreModifierKey, key.isEnabled());
}
}
- private static final String[] EVENTKEYS_POINTERTRACKER_ONDOWNEVENT = {
- "PointerTrackerOnDownEvent", "deltaT", "distanceSquared"
- };
+ /**
+ * Log a call to PointerTracker.onDownEvent().
+ *
+ * UserAction: The user has pressed down on a key.
+ * TODO: Differentiate with LatinIME.processMotionEvent.
+ */
+ private static final LogStatement LOGSTATEMENT_POINTERTRACKER_ONDOWNEVENT =
+ new LogStatement("PointerTrackerOnDownEvent", true, false, "deltaT", "distanceSquared");
public static void pointerTracker_onDownEvent(long deltaT, int distanceSquared) {
- final Object[] values = {
- deltaT, distanceSquared
- };
- getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_POINTERTRACKER_ONDOWNEVENT, values);
+ getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_ONDOWNEVENT, deltaT,
+ distanceSquared);
}
- private static final String[] EVENTKEYS_POINTERTRACKER_ONMOVEEVENT = {
- "PointerTrackerOnMoveEvent", "x", "y", "lastX", "lastY"
- };
+ /**
+ * Log a call to PointerTracker.onMoveEvent().
+ *
+ * UserAction: The user has moved their finger while pressing on the screen.
+ * TODO: Differentiate with LatinIME.processMotionEvent().
+ */
+ private static final LogStatement LOGSTATEMENT_POINTERTRACKER_ONMOVEEVENT =
+ new LogStatement("PointerTrackerOnMoveEvent", true, false, "x", "y", "lastX", "lastY");
public static void pointerTracker_onMoveEvent(final int x, final int y, final int lastX,
final int lastY) {
- final Object[] values = {
- x, y, lastX, lastY
- };
- getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_POINTERTRACKER_ONMOVEEVENT, values);
+ getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_ONMOVEEVENT, x, y, lastX, lastY);
}
- private static final String[] EVENTKEYS_RICHINPUTCONNECTION_COMMITCOMPLETION = {
- "RichInputConnectionCommitCompletion", "completionInfo"
- };
+ /**
+ * Log a call to RichInputConnection.commitCompletion().
+ *
+ * SystemResponse: The IME has committed a completion. A completion is an application-
+ * specific suggestion that is presented in a pop-up menu in the TextView.
+ */
+ private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_COMMITCOMPLETION =
+ new LogStatement("RichInputConnectionCommitCompletion", true, false, "completionInfo");
public static void richInputConnection_commitCompletion(final CompletionInfo completionInfo) {
- final Object[] values = {
- completionInfo
- };
final ResearchLogger researchLogger = getInstance();
- researchLogger.enqueuePotentiallyPrivateEvent(
- EVENTKEYS_RICHINPUTCONNECTION_COMMITCOMPLETION, values);
- }
-
- // Disabled for privacy-protection reasons. Because this event comes after
- // richInputConnection_commitText, which is the event used to separate LogUnits, the
- // data in this event can be associated with the next LogUnit, revealing information
- // about the current word even if it was supposed to be suppressed. The occurrance of
- // autocorrection can be determined by examining the difference between the text strings in
- // the last call to richInputConnection_setComposingText before
- // richInputConnection_commitText, so it's not a data loss.
- // TODO: Figure out how to log this event without loss of privacy.
- /*
- private static final String[] EVENTKEYS_RICHINPUTCONNECTION_COMMITCORRECTION = {
- "RichInputConnectionCommitCorrection", "typedWord", "autoCorrection"
- };
- */
- public static void richInputConnection_commitCorrection(CorrectionInfo correctionInfo) {
- /*
- final String typedWord = correctionInfo.getOldText().toString();
- final String autoCorrection = correctionInfo.getNewText().toString();
- final Object[] values = {
- scrubDigitsFromString(typedWord), scrubDigitsFromString(autoCorrection)
- };
+ researchLogger.enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_COMMITCOMPLETION,
+ completionInfo);
+ }
+
+ /**
+ * Log a call to RichInputConnection.revertDoubleSpacePeriod().
+ *
+ * SystemResponse: The IME has reverted ". ", which had previously replaced two typed spaces.
+ */
+ private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_REVERTDOUBLESPACEPERIOD =
+ new LogStatement("RichInputConnectionRevertDoubleSpacePeriod", false, false);
+ public static void richInputConnection_revertDoubleSpacePeriod() {
+ getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_REVERTDOUBLESPACEPERIOD);
+ }
+
+ /**
+ * Log a call to RichInputConnection.revertSwapPunctuation().
+ *
+ * SystemResponse: The IME has reverted a punctuation swap.
+ */
+ private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_REVERTSWAPPUNCTUATION =
+ new LogStatement("RichInputConnectionRevertSwapPunctuation", false, false);
+ public static void richInputConnection_revertSwapPunctuation() {
+ getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_REVERTSWAPPUNCTUATION);
+ }
+
+ /**
+ * Log a call to LatinIME.commitCurrentAutoCorrection().
+ *
+ * SystemResponse: The IME has committed an auto-correction. An auto-correction changes the raw
+ * text input to another word that the user more likely desired to type.
+ */
+ private static final LogStatement LOGSTATEMENT_LATINIME_COMMITCURRENTAUTOCORRECTION =
+ new LogStatement("LatinIMECommitCurrentAutoCorrection", true, true, "typedWord",
+ "autoCorrection", "separatorString");
+ public static void latinIme_commitCurrentAutoCorrection(final String typedWord,
+ final String autoCorrection, final String separatorString, final boolean isBatchMode,
+ final SuggestedWords suggestedWords) {
+ final String scrubbedTypedWord = scrubDigitsFromString(typedWord);
+ final String scrubbedAutoCorrection = scrubDigitsFromString(autoCorrection);
final ResearchLogger researchLogger = getInstance();
- researchLogger.enqueuePotentiallyPrivateEvent(
- EVENTKEYS_RICHINPUTCONNECTION_COMMITCORRECTION, values);
- */
+ researchLogger.mCurrentLogUnit.initializeSuggestions(suggestedWords);
+ researchLogger.commitCurrentLogUnitAsWord(scrubbedAutoCorrection, Long.MAX_VALUE,
+ isBatchMode);
+
+ // Add the autocorrection logStatement at the end of the logUnit for the committed word.
+ // We have to do this after calling commitCurrentLogUnitAsWord, because it may split the
+ // current logUnit, and then we have to peek to get the logUnit reference back.
+ final LogUnit logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit();
+ // TODO: Add test to confirm that the commitCurrentAutoCorrection log statement should
+ // always be added to logUnit (if non-null) and not mCurrentLogUnit.
+ researchLogger.enqueueEvent(logUnit, LOGSTATEMENT_LATINIME_COMMITCURRENTAUTOCORRECTION,
+ scrubbedTypedWord, scrubbedAutoCorrection, separatorString);
}
- private static final String[] EVENTKEYS_RICHINPUTCONNECTION_COMMITTEXT = {
- "RichInputConnectionCommitText", "typedWord", "newCursorPosition"
- };
- public static void richInputConnection_commitText(final CharSequence typedWord,
- final int newCursorPosition) {
- final String scrubbedWord = scrubDigitsFromString(typedWord.toString());
- final Object[] values = {
- scrubbedWord, newCursorPosition
- };
+ private boolean isExpectingCommitText = false;
+ /**
+ * Log a call to (UnknownClass).commitPartialText
+ *
+ * SystemResponse: The IME is committing part of a word. This happens if a space is
+ * automatically inserted to split a single typed string into two or more words.
+ */
+ // TODO: This method is currently unused. Find where it should be called from in the IME and
+ // add invocations.
+ private static final LogStatement LOGSTATEMENT_COMMIT_PARTIAL_TEXT =
+ new LogStatement("CommitPartialText", true, false, "newCursorPosition");
+ public static void commitPartialText(final String committedWord,
+ final long lastTimestampOfWordData, final boolean isBatchMode) {
final ResearchLogger researchLogger = getInstance();
- researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_RICHINPUTCONNECTION_COMMITTEXT,
- values);
- researchLogger.onWordComplete(scrubbedWord);
+ final String scrubbedWord = scrubDigitsFromString(committedWord);
+ researchLogger.enqueueEvent(LOGSTATEMENT_COMMIT_PARTIAL_TEXT);
+ researchLogger.mStatistics.recordAutoCorrection(SystemClock.uptimeMillis());
+ researchLogger.commitCurrentLogUnitAsWord(scrubbedWord, lastTimestampOfWordData,
+ isBatchMode);
}
- private static final String[] EVENTKEYS_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT = {
- "RichInputConnectionDeleteSurroundingText", "beforeLength", "afterLength"
- };
+ /**
+ * Log a call to RichInputConnection.commitText().
+ *
+ * SystemResponse: The IME is committing text. This happens after the user has typed a word
+ * and then a space or punctuation key.
+ */
+ private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTIONCOMMITTEXT =
+ new LogStatement("RichInputConnectionCommitText", true, false, "newCursorPosition");
+ public static void richInputConnection_commitText(final String committedWord,
+ final int newCursorPosition, final boolean isBatchMode) {
+ final ResearchLogger researchLogger = getInstance();
+ // Only include opening and closing logSegments if private data is included
+ final String scrubbedWord = scrubDigitsFromString(committedWord);
+ if (!researchLogger.isExpectingCommitText) {
+ researchLogger.enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTIONCOMMITTEXT,
+ newCursorPosition);
+ researchLogger.commitCurrentLogUnitAsWord(scrubbedWord, Long.MAX_VALUE, isBatchMode);
+ }
+ researchLogger.isExpectingCommitText = false;
+ }
+
+ /**
+ * Shared event for logging committed text.
+ */
+ private static final LogStatement LOGSTATEMENT_COMMITTEXT =
+ new LogStatement("CommitText", true, false, "committedText", "isBatchMode");
+ private void enqueueCommitText(final String word, final boolean isBatchMode) {
+ enqueueEvent(LOGSTATEMENT_COMMITTEXT, word, isBatchMode);
+ }
+
+ /**
+ * Log a call to RichInputConnection.deleteSurroundingText().
+ *
+ * SystemResponse: The IME has deleted text.
+ */
+ private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT =
+ new LogStatement("RichInputConnectionDeleteSurroundingText", true, false,
+ "beforeLength", "afterLength");
public static void richInputConnection_deleteSurroundingText(final int beforeLength,
final int afterLength) {
- final Object[] values = {
- beforeLength, afterLength
- };
- getInstance().enqueuePotentiallyPrivateEvent(
- EVENTKEYS_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT, values);
+ getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT,
+ beforeLength, afterLength);
}
- private static final String[] EVENTKEYS_RICHINPUTCONNECTION_FINISHCOMPOSINGTEXT = {
- "RichInputConnectionFinishComposingText"
- };
+ /**
+ * Log a call to RichInputConnection.finishComposingText().
+ *
+ * SystemResponse: The IME has left the composing text as-is.
+ */
+ private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_FINISHCOMPOSINGTEXT =
+ new LogStatement("RichInputConnectionFinishComposingText", false, false);
public static void richInputConnection_finishComposingText() {
- getInstance().enqueueEvent(EVENTKEYS_RICHINPUTCONNECTION_FINISHCOMPOSINGTEXT,
- EVENTKEYS_NULLVALUES);
+ getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_FINISHCOMPOSINGTEXT);
}
- private static final String[] EVENTKEYS_RICHINPUTCONNECTION_PERFORMEDITORACTION = {
- "RichInputConnectionPerformEditorAction", "imeActionNext"
- };
- public static void richInputConnection_performEditorAction(final int imeActionNext) {
- final Object[] values = {
- imeActionNext
- };
- getInstance().enqueueEvent(EVENTKEYS_RICHINPUTCONNECTION_PERFORMEDITORACTION, values);
+ /**
+ * Log a call to RichInputConnection.performEditorAction().
+ *
+ * SystemResponse: The IME is invoking an action specific to the editor.
+ */
+ private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_PERFORMEDITORACTION =
+ new LogStatement("RichInputConnectionPerformEditorAction", false, false,
+ "imeActionId");
+ public static void richInputConnection_performEditorAction(final int imeActionId) {
+ getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_PERFORMEDITORACTION,
+ imeActionId);
}
- private static final String[] EVENTKEYS_RICHINPUTCONNECTION_SENDKEYEVENT = {
- "RichInputConnectionSendKeyEvent", "eventTime", "action", "code"
- };
+ /**
+ * Log a call to RichInputConnection.sendKeyEvent().
+ *
+ * SystemResponse: The IME is telling the TextView that a key is being pressed through an
+ * alternate channel.
+ * TODO: only for hardware keys?
+ */
+ private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_SENDKEYEVENT =
+ new LogStatement("RichInputConnectionSendKeyEvent", true, false, "eventTime", "action",
+ "code");
public static void richInputConnection_sendKeyEvent(final KeyEvent keyEvent) {
- final Object[] values = {
- keyEvent.getEventTime(),
- keyEvent.getAction(),
- keyEvent.getKeyCode()
- };
- getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_RICHINPUTCONNECTION_SENDKEYEVENT,
- values);
+ getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_SENDKEYEVENT,
+ keyEvent.getEventTime(), keyEvent.getAction(), keyEvent.getKeyCode());
}
- private static final String[] EVENTKEYS_RICHINPUTCONNECTION_SETCOMPOSINGTEXT = {
- "RichInputConnectionSetComposingText", "text", "newCursorPosition"
- };
+ /**
+ * Log a call to RichInputConnection.setComposingText().
+ *
+ * SystemResponse: The IME is setting the composing text. Happens each time a character is
+ * entered.
+ */
+ private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_SETCOMPOSINGTEXT =
+ new LogStatement("RichInputConnectionSetComposingText", true, true, "text",
+ "newCursorPosition");
public static void richInputConnection_setComposingText(final CharSequence text,
final int newCursorPosition) {
if (text == null) {
throw new RuntimeException("setComposingText is null");
}
- final Object[] values = {
- text, newCursorPosition
- };
- getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_RICHINPUTCONNECTION_SETCOMPOSINGTEXT,
- values);
+ getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_SETCOMPOSINGTEXT, text,
+ newCursorPosition);
}
- private static final String[] EVENTKEYS_RICHINPUTCONNECTION_SETSELECTION = {
- "RichInputConnectionSetSelection", "from", "to"
- };
+ /**
+ * Log a call to RichInputConnection.setSelection().
+ *
+ * SystemResponse: The IME is requesting that the selection change. User-initiated selection-
+ * change requests do not go through this method -- it's only when the system wants to change
+ * the selection.
+ */
+ private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_SETSELECTION =
+ new LogStatement("RichInputConnectionSetSelection", true, false, "from", "to");
public static void richInputConnection_setSelection(final int from, final int to) {
- final Object[] values = {
- from, to
- };
- getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_RICHINPUTCONNECTION_SETSELECTION,
- values);
+ getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_SETSELECTION, from, to);
}
- private static final String[] EVENTKEYS_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT = {
- "SuddenJumpingTouchEventHandlerOnTouchEvent", "motionEvent"
- };
+ /**
+ * Log a call to SuddenJumpingTouchEventHandler.onTouchEvent().
+ *
+ * SystemResponse: The IME has filtered input events in case of an erroneous sensor reading.
+ */
+ private static final LogStatement LOGSTATEMENT_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT =
+ new LogStatement("SuddenJumpingTouchEventHandlerOnTouchEvent", true, false,
+ "motionEvent");
public static void suddenJumpingTouchEventHandler_onTouchEvent(final MotionEvent me) {
if (me != null) {
- final Object[] values = {
- me.toString()
- };
- getInstance().enqueuePotentiallyPrivateEvent(
- EVENTKEYS_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT, values);
+ getInstance().enqueueEvent(LOGSTATEMENT_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT,
+ me.toString());
}
}
- private static final String[] EVENTKEYS_SUGGESTIONSTRIPVIEW_SETSUGGESTIONS = {
- "SuggestionStripViewSetSuggestions", "suggestedWords"
- };
+ /**
+ * Log a call to SuggestionsView.setSuggestions().
+ *
+ * SystemResponse: The IME is setting the suggestions in the suggestion strip.
+ */
+ private static final LogStatement LOGSTATEMENT_SUGGESTIONSTRIPVIEW_SETSUGGESTIONS =
+ new LogStatement("SuggestionStripViewSetSuggestions", true, true, "suggestedWords");
public static void suggestionStripView_setSuggestions(final SuggestedWords suggestedWords) {
if (suggestedWords != null) {
- final Object[] values = {
- suggestedWords
- };
- getInstance().enqueuePotentiallyPrivateEvent(
- EVENTKEYS_SUGGESTIONSTRIPVIEW_SETSUGGESTIONS, values);
+ getInstance().enqueueEvent(LOGSTATEMENT_SUGGESTIONSTRIPVIEW_SETSUGGESTIONS,
+ suggestedWords);
}
}
- private static final String[] EVENTKEYS_USER_TIMESTAMP = {
- "UserTimestamp"
- };
+ /**
+ * The user has indicated a particular point in the log that is of interest.
+ *
+ * UserAction: From direct menu invocation.
+ */
+ private static final LogStatement LOGSTATEMENT_USER_TIMESTAMP =
+ new LogStatement("UserTimestamp", false, false);
public void userTimestamp() {
- getInstance().enqueueEvent(EVENTKEYS_USER_TIMESTAMP, EVENTKEYS_NULLVALUES);
+ getInstance().enqueueEvent(LOGSTATEMENT_USER_TIMESTAMP);
}
- private static final String[] EVENTKEYS_STATISTICS = {
- "Statistics", "charCount", "letterCount", "numberCount", "spaceCount", "deleteOpsCount",
- "wordCount", "isEmptyUponStarting", "isEmptinessStateKnown", "averageTimeBetweenKeys",
- "averageTimeBeforeDelete", "averageTimeDuringRepeatedDelete", "averageTimeAfterDelete"
- };
+ /**
+ * Log a call to LatinIME.onEndBatchInput().
+ *
+ * SystemResponse: The system has completed a gesture.
+ */
+ private static final LogStatement LOGSTATEMENT_LATINIME_ONENDBATCHINPUT =
+ new LogStatement("LatinIMEOnEndBatchInput", true, false, "enteredText",
+ "enteredWordPos");
+ public static void latinIME_onEndBatchInput(final CharSequence enteredText,
+ final int enteredWordPos, final SuggestedWords suggestedWords) {
+ final ResearchLogger researchLogger = getInstance();
+ researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_ONENDBATCHINPUT, enteredText,
+ enteredWordPos);
+ researchLogger.mCurrentLogUnit.initializeSuggestions(suggestedWords);
+ researchLogger.mStatistics.recordGestureInput(enteredText.length(),
+ SystemClock.uptimeMillis());
+ }
+
+ /**
+ * Log a call to LatinIME.handleBackspace() that is not a batch delete.
+ *
+ * UserInput: The user is deleting one or more characters by hitting the backspace key once.
+ * The covers single character deletes as well as deleting selections.
+ */
+ private static final LogStatement LOGSTATEMENT_LATINIME_HANDLEBACKSPACE =
+ new LogStatement("LatinIMEHandleBackspace", true, false, "numCharacters");
+ public static void latinIME_handleBackspace(final int numCharacters) {
+ final ResearchLogger researchLogger = getInstance();
+ researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_HANDLEBACKSPACE, numCharacters);
+ }
+
+ /**
+ * Log a call to LatinIME.handleBackspace() that is a batch delete.
+ *
+ * UserInput: The user is deleting a gestured word by hitting the backspace key once.
+ */
+ private static final LogStatement LOGSTATEMENT_LATINIME_HANDLEBACKSPACE_BATCH =
+ new LogStatement("LatinIMEHandleBackspaceBatch", true, false, "deletedText",
+ "numCharacters");
+ public static void latinIME_handleBackspace_batch(final CharSequence deletedText,
+ final int numCharacters) {
+ final ResearchLogger researchLogger = getInstance();
+ researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_HANDLEBACKSPACE_BATCH, deletedText,
+ numCharacters);
+ researchLogger.mStatistics.recordGestureDelete(deletedText.length(),
+ SystemClock.uptimeMillis());
+ }
+
+ /**
+ * Log a long interval between user operation.
+ *
+ * UserInput: The user has not done anything for a while.
+ */
+ private static final LogStatement LOGSTATEMENT_ONUSERPAUSE = new LogStatement("OnUserPause",
+ false, false, "intervalInMs");
+ public static void onUserPause(final long interval) {
+ final ResearchLogger researchLogger = getInstance();
+ researchLogger.enqueueEvent(LOGSTATEMENT_ONUSERPAUSE, interval);
+ }
+
+ /**
+ * Record the current time in case the LogUnit is later split.
+ *
+ * If the current logUnit is split, then tapping, motion events, etc. before this time should
+ * be assigned to one LogUnit, and events after this time should go into the following LogUnit.
+ */
+ public static void recordTimeForLogUnitSplit() {
+ final ResearchLogger researchLogger = getInstance();
+ researchLogger.setSavedDownEventTime(SystemClock.uptimeMillis());
+ researchLogger.mSavedDownEventTime = Long.MAX_VALUE;
+ }
+
+ /**
+ * Log a call to LatinIME.handleSeparator()
+ *
+ * SystemResponse: The system is inserting a separator character, possibly performing auto-
+ * correction or other actions appropriate at the end of a word.
+ */
+ private static final LogStatement LOGSTATEMENT_LATINIME_HANDLESEPARATOR =
+ new LogStatement("LatinIMEHandleSeparator", false, false, "primaryCode",
+ "isComposingWord");
+ public static void latinIME_handleSeparator(final int primaryCode,
+ final boolean isComposingWord) {
+ final ResearchLogger researchLogger = getInstance();
+ researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_HANDLESEPARATOR, primaryCode,
+ isComposingWord);
+ }
+
+ /**
+ * Log statistics.
+ *
+ * ContextualData, recorded at the end of a session.
+ */
+ private static final LogStatement LOGSTATEMENT_STATISTICS =
+ new LogStatement("Statistics", false, false, "charCount", "letterCount", "numberCount",
+ "spaceCount", "deleteOpsCount", "wordCount", "isEmptyUponStarting",
+ "isEmptinessStateKnown", "averageTimeBetweenKeys", "averageTimeBeforeDelete",
+ "averageTimeDuringRepeatedDelete", "averageTimeAfterDelete",
+ "dictionaryWordCount", "splitWordsCount", "gestureInputCount",
+ "gestureCharsCount", "gesturesDeletedCount", "manualSuggestionsCount",
+ "revertCommitsCount", "correctedWordsCount", "autoCorrectionsCount");
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);
+ researchLogger.enqueueEvent(LOGSTATEMENT_STATISTICS, 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(),
+ statistics.mDictionaryWordCount, statistics.mSplitWordsCount,
+ statistics.mGesturesInputCount, statistics.mGesturesCharsCount,
+ statistics.mGesturesDeletedCount, statistics.mManualSuggestionsCount,
+ statistics.mRevertCommitsCount, statistics.mCorrectedWordsCount,
+ statistics.mAutoCorrectionsCount);
}
}
diff --git a/java/src/com/android/inputmethod/research/Statistics.java b/java/src/com/android/inputmethod/research/Statistics.java
index eab465aa2..50e2b7fbc 100644
--- a/java/src/com/android/inputmethod/research/Statistics.java
+++ b/java/src/com/android/inputmethod/research/Statistics.java
@@ -1,24 +1,31 @@
/*
* 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
+ * 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
+ * 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.
+ * 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;
+import android.util.Log;
+
+import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.define.ProductionFlag;
public class Statistics {
+ private static final String TAG = Statistics.class.getSimpleName();
+ private static final boolean DEBUG = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG;
+
+ // TODO: Cleanup comments to only including those giving meaningful information.
// Number of characters entered during a typing session
int mCharCount;
// Number of letter characters entered during a typing session
@@ -31,6 +38,24 @@ public class Statistics {
int mDeleteKeyCount;
// Number of words entered during a session.
int mWordCount;
+ // Number of words found in the dictionary.
+ int mDictionaryWordCount;
+ // Number of words split and spaces automatically entered.
+ int mSplitWordsCount;
+ // Number of words entered during a session.
+ int mCorrectedWordsCount;
+ // Number of gestures that were input.
+ int mGesturesInputCount;
+ // Number of gestures that were deleted.
+ int mGesturesDeletedCount;
+ // Total number of characters in words entered by gesture.
+ int mGesturesCharsCount;
+ // Number of manual suggestions chosen.
+ int mManualSuggestionsCount;
+ // Number of times that autocorrection was invoked.
+ int mAutoCorrectionsCount;
+ // Number of times a commit was reverted in this session.
+ int mRevertCommitsCount;
// Whether the text field was empty upon editing
boolean mIsEmptyUponStarting;
boolean mIsEmptinessStateKnown;
@@ -91,29 +116,34 @@ public class Statistics {
mSpaceCount = 0;
mDeleteKeyCount = 0;
mWordCount = 0;
+ mDictionaryWordCount = 0;
+ mSplitWordsCount = 0;
+ mCorrectedWordsCount = 0;
+ mGesturesInputCount = 0;
+ mGesturesDeletedCount = 0;
+ mManualSuggestionsCount = 0;
+ mRevertCommitsCount = 0;
+ mAutoCorrectionsCount = 0;
mIsEmptyUponStarting = true;
mIsEmptinessStateKnown = false;
mKeyCounter.reset();
mBeforeDeleteKeyCounter.reset();
mDuringRepeatedDeleteKeysCounter.reset();
mAfterDeleteKeyCounter.reset();
+ mGesturesCharsCount = 0;
+ mGesturesDeletedCount = 0;
mLastTapTime = 0;
mIsLastKeyDeleteKey = false;
}
public void recordChar(int codePoint, long time) {
- final long delta = time - mLastTapTime;
- if (codePoint == Keyboard.CODE_DELETE) {
+ if (DEBUG) {
+ Log.d(TAG, "recordChar() called");
+ }
+ if (codePoint == Constants.CODE_DELETE) {
mDeleteKeyCount++;
- if (delta < MIN_DELETION_INTERMISSION) {
- if (mIsLastKeyDeleteKey) {
- mDuringRepeatedDeleteKeysCounter.add(delta);
- } else {
- mBeforeDeleteKeyCounter.add(delta);
- }
- }
- mIsLastKeyDeleteKey = true;
+ recordUserAction(time, true /* isDeletion */);
} else {
mCharCount++;
if (Character.isDigit(codePoint)) {
@@ -125,22 +155,78 @@ public class Statistics {
if (Character.isSpaceChar(codePoint)) {
mSpaceCount++;
}
- if (mIsLastKeyDeleteKey && delta < MIN_DELETION_INTERMISSION) {
- mAfterDeleteKeyCounter.add(delta);
- } else if (!mIsLastKeyDeleteKey && delta < MIN_TYPING_INTERMISSION) {
- mKeyCounter.add(delta);
- }
- mIsLastKeyDeleteKey = false;
+ recordUserAction(time, false /* isDeletion */);
}
- mLastTapTime = time;
}
- public void recordWordEntered() {
+ public void recordWordEntered(final boolean isDictionaryWord,
+ final boolean containsCorrection) {
mWordCount++;
+ if (isDictionaryWord) {
+ mDictionaryWordCount++;
+ }
+ if (containsCorrection) {
+ mCorrectedWordsCount++;
+ }
+ }
+
+ public void recordSplitWords() {
+ mSplitWordsCount++;
+ }
+
+ public void recordGestureInput(final int numCharsEntered, final long time) {
+ mGesturesInputCount++;
+ mGesturesCharsCount += numCharsEntered;
+ recordUserAction(time, false /* isDeletion */);
}
public void setIsEmptyUponStarting(final boolean isEmpty) {
mIsEmptyUponStarting = isEmpty;
mIsEmptinessStateKnown = true;
}
+
+ public void recordGestureDelete(final int length, final long time) {
+ mGesturesDeletedCount++;
+ recordUserAction(time, true /* isDeletion */);
+ }
+
+ public void recordManualSuggestion(final long time) {
+ mManualSuggestionsCount++;
+ recordUserAction(time, false /* isDeletion */);
+ }
+
+ public void recordAutoCorrection(final long time) {
+ mAutoCorrectionsCount++;
+ recordUserAction(time, false /* isDeletion */);
+ }
+
+ public void recordRevertCommit(final long time) {
+ mRevertCommitsCount++;
+ recordUserAction(time, true /* isDeletion */);
+ }
+
+ private void recordUserAction(final long time, final boolean isDeletion) {
+ final long delta = time - mLastTapTime;
+ if (isDeletion) {
+ if (delta < MIN_DELETION_INTERMISSION) {
+ if (mIsLastKeyDeleteKey) {
+ mDuringRepeatedDeleteKeysCounter.add(delta);
+ } else {
+ mBeforeDeleteKeyCounter.add(delta);
+ }
+ } else {
+ ResearchLogger.onUserPause(delta);
+ }
+ } else {
+ if (mIsLastKeyDeleteKey && delta < MIN_DELETION_INTERMISSION) {
+ mAfterDeleteKeyCounter.add(delta);
+ } else if (!mIsLastKeyDeleteKey && delta < MIN_TYPING_INTERMISSION) {
+ mKeyCounter.add(delta);
+ } else {
+ ResearchLogger.onUserPause(delta);
+ }
+ }
+ mIsLastKeyDeleteKey = isDeletion;
+ mLastTapTime = time;
+ }
}
diff --git a/java/src/com/android/inputmethod/research/UploaderService.java b/java/src/com/android/inputmethod/research/UploaderService.java
index 7a5749096..89c67fbb2 100644
--- a/java/src/com/android/inputmethod/research/UploaderService.java
+++ b/java/src/com/android/inputmethod/research/UploaderService.java
@@ -1,17 +1,17 @@
/*
* 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
+ * 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
+ * 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.
+ * 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;
@@ -30,6 +30,7 @@ import android.os.Bundle;
import android.util.Log;
import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.define.ProductionFlag;
import java.io.BufferedReader;
import java.io.File;
@@ -45,8 +46,12 @@ import java.net.URL;
public final class UploaderService extends IntentService {
private static final String TAG = UploaderService.class.getSimpleName();
+ private static final boolean DEBUG = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG;
+ // Set IS_INHIBITING_AUTO_UPLOAD to true for local testing
+ private static final boolean IS_INHIBITING_AUTO_UPLOAD = false
+ && ProductionFlag.IS_EXPERIMENTAL_DEBUG; // Force false in production
public static final long RUN_INTERVAL = AlarmManager.INTERVAL_HOUR;
- private static final String EXTRA_UPLOAD_UNCONDITIONALLY = UploaderService.class.getName()
+ public static final String EXTRA_UPLOAD_UNCONDITIONALLY = UploaderService.class.getName()
+ ".extra.UPLOAD_UNCONDITIONALLY";
private static final int BUF_SIZE = 1024 * 8;
protected static final int TIMEOUT_IN_MS = 1000 * 4;
@@ -116,7 +121,8 @@ public final class UploaderService extends IntentService {
}
private void doUpload(final boolean isUploadingUnconditionally) {
- if (!isUploadingUnconditionally && (!isExternallyPowered() || !hasWifiConnection())) {
+ if (!isUploadingUnconditionally && (!isExternallyPowered() || !hasWifiConnection()
+ || IS_INHIBITING_AUTO_UPLOAD)) {
return;
}
if (mFilesDir == null) {
@@ -125,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();
}
});
@@ -141,7 +147,9 @@ public final class UploaderService extends IntentService {
}
private boolean uploadFile(File file) {
- Log.d(TAG, "attempting upload of " + file.getAbsolutePath());
+ if (DEBUG) {
+ Log.d(TAG, "attempting upload of " + file.getAbsolutePath());
+ }
boolean success = false;
final int contentLength = (int) file.length();
HttpURLConnection connection = null;
@@ -157,6 +165,9 @@ public final class UploaderService extends IntentService {
int numBytesRead;
while ((numBytesRead = fileInputStream.read(buf)) != -1) {
os.write(buf, 0, numBytesRead);
+ if (DEBUG) {
+ Log.d(TAG, new String(buf));
+ }
}
if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
Log.d(TAG, "upload failed: " + connection.getResponseCode());
@@ -171,7 +182,9 @@ public final class UploaderService extends IntentService {
}
file.delete();
success = true;
- Log.d(TAG, "upload successful");
+ if (DEBUG) {
+ Log.d(TAG, "upload successful");
+ }
} catch (Exception e) {
e.printStackTrace();
} finally {