diff options
Diffstat (limited to 'java/src/com/android/inputmethod/research')
8 files changed, 1014 insertions, 552 deletions
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..cb331d7f9 --- /dev/null +++ b/java/src/com/android/inputmethod/research/JsonUtils.java @@ -0,0 +1,103 @@ +/* + * 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.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; + +/* 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.getWordInfo(j); + jsonWriter.value(wordInfo.toString()); + } + 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..a3c3e89de 100644 --- a/java/src/com/android/inputmethod/research/LogBuffer.java +++ b/java/src/com/android/inputmethod/research/LogBuffer.java @@ -110,4 +110,8 @@ public class LogBuffer { } return logUnit; } + + public boolean isEmpty() { + return mLogUnits.isEmpty(); + } } diff --git a/java/src/com/android/inputmethod/research/LogUnit.java b/java/src/com/android/inputmethod/research/LogUnit.java index d8b3a29ff..27c4027de 100644 --- a/java/src/com/android/inputmethod/research/LogUnit.java +++ b/java/src/com/android/inputmethod/research/LogUnit.java @@ -16,9 +16,23 @@ package com.android.inputmethod.research; -import com.android.inputmethod.latin.CollectionUtils; +import android.content.SharedPreferences; +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.Utils; +import com.android.inputmethod.latin.define.ProductionFlag; +import com.android.inputmethod.research.ResearchLogger.LogStatement; + +import java.io.IOException; +import java.io.StringWriter; import java.util.ArrayList; +import java.util.List; +import java.util.Map; /** * A group of log statements related to each other. @@ -35,28 +49,163 @@ 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; private String mWord; - private boolean mContainsDigit; + private boolean mMayContainDigit; + private boolean mIsPartOfMegaword; + + public LogUnit() { + mLogStatementList = new ArrayList<LogStatement>(); + mValuesList = new ArrayList<Object[]>(); + mTimeList = new ArrayList<Long>(); + mIsPartOfMegaword = false; + } - public void addLogStatement(final String[] keys, final Object[] values, - final Boolean isPotentiallyPrivate) { - mKeysList.add(keys); + 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; + } + + 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(); + /** + * Publish the contents of this LogUnit to researchLog. + */ + public synchronized void publishTo(final ResearchLog researchLog, + final boolean isIncludingPrivateData) { + // 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; + } + final int size = mLogStatementList.size(); + // Write out any logStatement that passes the privacy filter. for (int i = 0; i < size; i++) { - if (!mIsPotentiallyPrivate.get(i) || isIncludingPrivateData) { - researchLog.outputEvent(mKeysList.get(i), mValuesList.get(i)); + final LogStatement logStatement = mLogStatementList.get(i); + if (!isIncludingPrivateData && logStatement.mIsPotentiallyPrivate) { + continue; + } + if (mIsPartOfMegaword && logStatement.mIsPotentiallyRevealing) { + continue; + } + // Only retrieve the jsonWriter if we need to. If we don't get this far, then + // researchLog.getValidJsonWriter() will not open the file for writing. + final JsonWriter jsonWriter = researchLog.getValidJsonWriterLocked(); + outputLogStatementToLocked(jsonWriter, mLogStatementList.get(i), mValuesList.get(i), + mTimeList.get(i)); + if (DEBUG) { + outputLogStatementToLocked(debugJsonWriter, mLogStatementList.get(i), + mValuesList.get(i), mTimeList.get(i)); + } + } + 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 CURRENT_TIME_KEY = "_ct"; + private static final String UPTIME_KEY = "_ut"; + private static final String EVENT_TYPE_KEY = "_ty"; + + /** + * Write the logStatement and its contents out through jsonWriter. + * + * Note that this method is not thread safe for the same jsonWriter. Callers must ensure + * thread safety. + */ + private boolean outputLogStatementToLocked(final JsonWriter jsonWriter, + final LogStatement logStatement, final Object[] values, final Long time) { + if (DEBUG) { + if (logStatement.mKeys.length != values.length) { + Log.d(TAG, "Key and Value list sizes do not match. " + logStatement.mName); + } + } + try { + jsonWriter.beginObject(); + jsonWriter.name(CURRENT_TIME_KEY).value(System.currentTimeMillis()); + jsonWriter.name(UPTIME_KEY).value(time); + jsonWriter.name(EVENT_TYPE_KEY).value(logStatement.mName); + final String[] keys = logStatement.mKeys; + final int length = values.length; + for (int i = 0; i < length; i++) { + jsonWriter.name(keys[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 == null) { + jsonWriter.nullValue(); + } else { + 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; + } + public void setWord(String word) { mWord = word; } @@ -69,15 +218,50 @@ import java.util.ArrayList; return mWord != null; } - public void setContainsDigit() { - mContainsDigit = true; + public void setMayContainDigit() { + mMayContainDigit = true; } - public boolean hasDigit() { - return mContainsDigit; + public boolean mayContainDigit() { + return mMayContainDigit; } 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(); + 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; + + // Purge the logStatements and associated data from this LogUnit. + laterLogStatements.clear(); + laterValues.clear(); + laterTimes.clear(); + mIsPartOfMegaword = true; + + return newLogUnit; + } + } + return new LogUnit(); } } diff --git a/java/src/com/android/inputmethod/research/MainLogBuffer.java b/java/src/com/android/inputmethod/research/MainLogBuffer.java index 745768d35..0185e5fc0 100644 --- a/java/src/com/android/inputmethod/research/MainLogBuffer.java +++ b/java/src/com/android/inputmethod/research/MainLogBuffer.java @@ -16,16 +16,23 @@ 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.Random; public class MainLogBuffer extends LogBuffer { + 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; + private static final int DEFAULT_NUMBER_OF_WORDS_BETWEEN_SAMPLES = + ProductionFlag.IS_EXPERIMENTAL_DEBUG ? 2 : 18; private final ResearchLog mResearchLog; private Suggest mSuggest; @@ -61,6 +68,9 @@ public class MainLogBuffer extends LogBuffer { mWordsUntilSafeToSample--; } } + if (DEBUG) { + Log.d(TAG, "shiftedIn " + (newLogUnit.hasWord() ? newLogUnit.getWord() : "")); + } } public void resetWordCounter() { @@ -104,12 +114,16 @@ public class MainLogBuffer extends LogBuffer { final String word = logUnit.getWord(); if (word == null) { // Digits outside words are a privacy threat. - if (logUnit.hasDigit()) { + if (logUnit.mayContainDigit()) { return false; } } else { // 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; } } diff --git a/java/src/com/android/inputmethod/research/ResearchLog.java b/java/src/com/android/inputmethod/research/ResearchLog.java index 70c38e909..a6b1b889f 100644 --- a/java/src/com/android/inputmethod/research/ResearchLog.java +++ b/java/src/com/android/inputmethod/research/ResearchLog.java @@ -16,15 +16,9 @@ package com.android.inputmethod.research; -import android.content.SharedPreferences; -import android.os.SystemClock; 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; @@ -33,7 +27,6 @@ import java.io.FileWriter; 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,7 +44,7 @@ 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; @@ -203,105 +196,17 @@ public class ResearchLog { } } - 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 getValidJsonWriterLocked() { try { if (mJsonWriter == NULL_JSON_WRITER) { mJsonWriter = new JsonWriter(new BufferedWriter(new FileWriter(mFile))); mJsonWriter.beginArray(); 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"); @@ -316,5 +221,6 @@ public class ResearchLog { mJsonWriter = NULL_JSON_WRITER; } } + return mJsonWriter; } } diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java index 763fd6e00..cde100a5f 100644 --- a/java/src/com/android/inputmethod/research/ResearchLogger.java +++ b/java/src/com/android/inputmethod/research/ResearchLogger.java @@ -34,7 +34,6 @@ 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.IBinder; @@ -47,7 +46,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; @@ -57,9 +55,9 @@ import com.android.inputmethod.keyboard.Keyboard; import com.android.inputmethod.keyboard.KeyboardId; 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; @@ -84,19 +82,29 @@ import java.util.UUID; */ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChangeListener { 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; + // Whether all n-grams should be logged. true will disclose private info. + private static final boolean LOG_EVERYTHING = 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; 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"; private static final SimpleDateFormat TIMESTAMP_DATEFORMAT = new SimpleDateFormat("yyyyMMddHHmmssS", Locale.US); + // Whether to show an indicator on the screen that logging is on. Currently a very small red + // 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; + // 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 || LOG_EVERYTHING; public static final int FEEDBACK_WORD_BUFFER_SIZE = 5; // constants related to specific log points @@ -137,13 +145,11 @@ 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; + private LatinIME mLatinIME; private final Statistics mStatistics; private Intent mUploadIntent; - private PendingIntent mUploadPendingIntent; private LogUnit mCurrentLogUnit = new LogUnit(); @@ -155,12 +161,12 @@ 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 SharedPreferences prefs) { + 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."); } @@ -185,13 +191,12 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang e.apply(); } } - mInputMethodService = ims; + mLatinIME = latinIME; mPrefs = prefs; - mUploadIntent = new Intent(mInputMethodService, UploaderService.class); - mUploadPendingIntent = PendingIntent.getService(mInputMethodService, 0, mUploadIntent, 0); + mUploadIntent = new Intent(mLatinIME, UploaderService.class); if (ProductionFlag.IS_EXPERIMENTAL) { - scheduleUploadingService(mInputMethodService); + scheduleUploadingService(mLatinIME); } } @@ -250,7 +255,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 +270,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 +283,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang new OnCancelListener() { @Override public void onCancel(DialogInterface dialog) { - mInputMethodService.requestHideSelf(0); + mLatinIME.requestHideSelf(0); } }); mSplashDialog = builder.create(); @@ -322,10 +327,10 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } private void checkForEmptyEditor() { - if (mInputMethodService == null) { + if (mLatinIME == null) { return; } - final InputConnection ic = mInputMethodService.getCurrentInputConnection(); + final InputConnection ic = mLatinIME.getCurrentInputConnection(); if (ic == null) { return; } @@ -378,11 +383,11 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang if (DEBUG) { Log.d(TAG, "stop called"); } - logStatistics(); commitCurrentLogUnit(); if (mMainLogBuffer != null) { - publishLogBuffer(mMainLogBuffer, mMainResearchLog, false /* isIncludingPrivateData */); + publishLogBuffer(mMainLogBuffer, mMainResearchLog, + LOG_EVERYTHING /* isIncludingPrivateData */); mMainResearchLog.close(null /* callback */); mMainLogBuffer = null; } @@ -427,21 +432,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) { @@ -539,9 +529,40 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } */ - private static final String[] EVENTKEYS_FEEDBACK = { - "UserTimestamp", "contents" - }; + static class LogStatement { + final String mName; + + // mIsPotentiallyPrivate indicates that event contains potentially private information. If + // the word that this event is a part of is determined to be privacy-sensitive, then this + // event should not be included in the output log. The system waits to output until the + // containing word is known. + final boolean mIsPotentiallyPrivate; + + // mIsPotentiallyRevealing indicates that this statement may disclose details about other + // words typed in other LogUnits. This can happen if the user is not inserting spaces, and + // data from Suggestions and/or Composing text reveals the entire "megaword". For example, + // say the user is typing "for the win", and the system wants to record the bigram "the + // win". If the user types "forthe", omitting the space, the system will give "for the" as + // a suggestion. If the user accepts the autocorrection, the suggestion for "for the" is + // included in the log for the word "the", disclosing that the previous word had been "for". + // For now, we simply do not include this data when logging part of a "megaword". + final boolean mIsPotentiallyRevealing; + + // mKeys stores the names that are the attributes in the output json objects + final String[] mKeys; + private static final String[] NULL_KEYS = new String[0]; + + LogStatement(final String name, final boolean isPotentiallyPrivate, + final boolean isPotentiallyRevealing, final String... keys) { + mName = name; + mIsPotentiallyPrivate = isPotentiallyPrivate; + mIsPotentiallyRevealing = isPotentiallyRevealing; + mKeys = (keys == null) ? NULL_KEYS : keys; + } + } + + private static final LogStatement LOGSTATEMENT_FEEDBACK = + new LogStatement("UserTimestamp", false, false, "contents"); public void sendFeedback(final String feedbackContents, final boolean includeHistory) { if (mFeedbackLogBuffer == null) { return; @@ -552,11 +573,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang mFeedbackLogBuffer.clear(); } final LogUnit feedbackLogUnit = new LogUnit(); - final Object[] values = { - feedbackContents - }; - feedbackLogUnit.addLogStatement(EVENTKEYS_FEEDBACK, values, - false /* isPotentiallyPrivate */); + feedbackLogUnit.addLogStatement(LOGSTATEMENT_FEEDBACK, SystemClock.uptimeMillis(), + feedbackContents); mFeedbackLogBuffer.shiftIn(feedbackLogUnit); publishLogBuffer(mFeedbackLogBuffer, mFeedbackLog, true /* isIncludingPrivateData */); mFeedbackLog.close(new Runnable() { @@ -572,7 +590,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang if (DEBUG) { Log.d(TAG, "calling uploadNow()"); } - mInputMethodService.startService(mUploadIntent); + mLatinIME.startService(mUploadIntent); } public void onLeavingSendFeedbackDialog() { @@ -586,6 +604,13 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } } + private Dictionary getDictionary() { + if (mSuggest == null) { + return null; + } + return mSuggest.getMainDictionary(); + } + private void setIsPasswordView(boolean isPasswordView) { mIsPasswordView = isPasswordView; } @@ -626,7 +651,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang 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, @@ -641,58 +667,30 @@ 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; + private synchronized void enqueueEvent(LogStatement logStatement, Object... values) { + assert values.length == logStatement.mKeys.length; if (isAllowedToLog()) { - mCurrentLogUnit.addLogStatement(keys, values, true /* isPotentiallyPrivate */); + final long time = SystemClock.uptimeMillis(); + mCurrentLogUnit.addLogStatement(logStatement, time, values); } } private void setCurrentLogUnitContainsDigitFlag() { - mCurrentLogUnit.setContainsDigit(); - } - - /** - * 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 */); - } + mCurrentLogUnit.setMayContainDigit(); } /* 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) { + if ((mMainLogBuffer.isSafeToLog() || LOG_EVERYTHING) && mMainResearchLog != null) { publishLogBuffer(mMainLogBuffer, mMainResearchLog, true /* isIncludingPrivateData */); mMainLogBuffer.resetWordCounter(); @@ -702,36 +700,59 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang mFeedbackLogBuffer.shiftIn(mCurrentLogUnit); } mCurrentLogUnit = new LogUnit(); - Log.d(TAG, "commitCurrentLogUnit"); } } + 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); /* package for test */ void publishLogBuffer(final LogBuffer logBuffer, final ResearchLog researchLog, final boolean isIncludingPrivateData) { + final LogUnit openingLogUnit = new LogUnit(); + if (logBuffer.isEmpty()) return; + openingLogUnit.addLogStatement(LOGSTATEMENT_LOG_SEGMENT_OPENING, SystemClock.uptimeMillis(), + isIncludingPrivateData); + researchLog.publish(openingLogUnit, true /* isIncludingPrivateData */); LogUnit logUnit; while ((logUnit = logBuffer.shiftOut()) != null) { + if (DEBUG) { + Log.d(TAG, "publishLogBuffer: " + (logUnit.hasWord() ? logUnit.getWord() + : "<wordless>")); + } researchLog.publish(logUnit, isIncludingPrivateData); } + 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)) { + private static final LogStatement LOGSTATEMENT_COMMIT_RECORD_SPLIT_WORDS = + new LogStatement("recordSplitWords", true, false); + public void onWordComplete(final String word, final long maxTime) { + final Dictionary dictionary = getDictionary(); + if (word != null && word.length() > 0 && hasLetters(word)) { mCurrentLogUnit.setWord(word); - mStatistics.recordWordEntered(); + final boolean isDictionaryWord = dictionary != null + && dictionary.isValidWord(word); + mStatistics.recordWordEntered(isDictionaryWord); } + final LogUnit newLogUnit = mCurrentLogUnit.splitByTime(maxTime); + enqueueCommitText(word); commitCurrentLogUnit(); + mCurrentLogUnit = newLogUnit; } private static int scrubDigitFromCodePoint(int codePoint) { @@ -775,70 +796,90 @@ 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, LOG_EVERYTHING, + ProductionFlag.IS_EXPERIMENTAL_DEBUG); } catch (NameNotFoundException e) { e.printStackTrace(); } } } - public void latinIME_onFinishInputInternal() { + public void latinIME_onFinishInputViewInternal() { + logStatistics(); 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("MainKeyboardViewProcessMotionEvent", true, false, "action", + "eventTime", "id", "x", "y", "size", "pressure"); 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) { @@ -855,40 +896,44 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } 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); + getInstance().enqueueEvent(LOGSTATEMENT_MAIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENT, + actionString, eventTime, id, x, y, size, pressure); } } - 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 +942,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 +981,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 +1029,471 @@ 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.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"); 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 String scrubbedWord = scrubDigitsFromString(suggestion); final ResearchLogger researchLogger = getInstance(); - researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_PICKSUGGESTIONMANUALLY, - values); - } - - 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); + researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_PICKSUGGESTIONMANUALLY, + scrubDigitsFromString(replacedWord), index, + suggestion == null ? null : scrubbedWord, Constants.SUGGESTION_STRIP_COORDINATE, + Constants.SUGGESTION_STRIP_COORDINATE); + researchLogger.onWordComplete(scrubbedWord, Long.MAX_VALUE); + researchLogger.mStatistics.recordManualSuggestion(); + } + + /** + * 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"); + public static void latinIME_punctuationSuggestion(final int index, final String suggestion) { + final ResearchLogger researchLogger = getInstance(); + researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_PUNCTUATIONSUGGESTION, index, suggestion, + Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE); + researchLogger.onWordComplete(suggestion, Long.MAX_VALUE); } - private static final String[] EVENTKEYS_LATINIME_SENDKEYCODEPOINT = { - "LatinIMESendKeyCodePoint", "code" - }; + /** + * Log a call to LatinIME.sendKeyCodePoint(). + * + * SystemResponse: The IME is simulating a hardware keypress. This happens for numbers; other + * input typically goes through RichInputConnection.setComposingText() and + * RichInputConnection.commitText(). + */ + 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" - }; + /** + * 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); public static void latinIME_swapSwapperAndSpace() { - getInstance().enqueueEvent(EVENTKEYS_LATINIME_SWAPSWAPPERANDSPACE, EVENTKEYS_NULLVALUES); + getInstance().enqueueEvent(LOGSTATEMENT_LATINIME_SWAPSWAPPERANDSPACE); } - private static final String[] EVENTKEYS_MAINKEYBOARDVIEW_ONLONGPRESS = { - "MainKeyboardViewOnLongPress" - }; + /** + * 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(); + getInstance().setIsPasswordView(isPasswordView); + getInstance().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"); + public static void latinIME_revertCommit(final String committedWord, + final String originallyTypedWord) { + final ResearchLogger researchLogger = getInstance(); + researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_REVERTCOMMIT, committedWord, + originallyTypedWord); + researchLogger.mStatistics.recordRevertCommit(); } - 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); + getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCODEINPUT, + Constants.printableCode(scrubDigitFromCodePoint(code)), + outputText == null ? null : scrubDigitsFromString(outputText.toString()), + x, y, ignoreModifierKey, altersCode, key.isEnabled()); } } - private static final String[] EVENTKEYS_POINTERTRACKER_CALLLISTENERONRELEASE = { - "PointerTrackerCallListenerOnRelease", "code", "withSliding", "ignoreModifierKey", - "isEnabled" - }; + /** + * 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 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, false, "typedWord", + "autoCorrection", "separatorString"); + public static void latinIme_commitCurrentAutoCorrection(final String typedWord, + final String autoCorrection, final String separatorString) { + final String scrubbedTypedWord = scrubDigitsFromString(typedWord); + final String scrubbedAutoCorrection = scrubDigitsFromString(autoCorrection); + final ResearchLogger researchLogger = getInstance(); + researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_COMMITCURRENTAUTOCORRECTION, + scrubbedTypedWord, scrubbedAutoCorrection, separatorString); + researchLogger.onWordComplete(scrubbedAutoCorrection, Long.MAX_VALUE); + } + + private boolean isExpectingCommitText = false; + /** + * Log a call to RichInputConnection.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_LATINIME_COMMIT_PARTIAL_TEXT = + new LogStatement("LatinIMECommitPartialText", true, false, "newCursorPosition"); + public static void latinIME_commitPartialText(final CharSequence committedWord, + final long lastTimestampOfWordData) { final ResearchLogger researchLogger = getInstance(); - researchLogger.enqueuePotentiallyPrivateEvent( - EVENTKEYS_RICHINPUTCONNECTION_COMMITCORRECTION, values); - */ + final String scrubbedWord = scrubDigitsFromString(committedWord.toString()); + researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_COMMIT_PARTIAL_TEXT); + researchLogger.onWordComplete(scrubbedWord, lastTimestampOfWordData); + researchLogger.mStatistics.recordSplitWords(); } - private static final String[] EVENTKEYS_RICHINPUTCONNECTION_COMMITTEXT = { - "RichInputConnectionCommitText", "typedWord", "newCursorPosition" - }; - public static void richInputConnection_commitText(final CharSequence typedWord, + /** + * 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 CharSequence committedWord, final int newCursorPosition) { - final String scrubbedWord = scrubDigitsFromString(typedWord.toString()); - final Object[] values = { - scrubbedWord, newCursorPosition - }; final ResearchLogger researchLogger = getInstance(); - researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_RICHINPUTCONNECTION_COMMITTEXT, - values); - researchLogger.onWordComplete(scrubbedWord); + final String scrubbedWord = scrubDigitsFromString(committedWord.toString()); + if (!researchLogger.isExpectingCommitText) { + researchLogger.enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTIONCOMMITTEXT, + newCursorPosition); + researchLogger.onWordComplete(scrubbedWord, Long.MAX_VALUE); + } + researchLogger.isExpectingCommitText = false; } - private static final String[] EVENTKEYS_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT = { - "RichInputConnectionDeleteSurroundingText", "beforeLength", "afterLength" - }; + /** + * Shared event for logging committed text. + */ + private static final LogStatement LOGSTATEMENT_COMMITTEXT = + new LogStatement("CommitText", true, false, "committedText"); + private void enqueueCommitText(final CharSequence word) { + enqueueEvent(LOGSTATEMENT_COMMITTEXT, word); + } + + /** + * 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); + } + + /** + * 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 ResearchLogger researchLogger = getInstance(); + researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_ONENDBATCHINPUT, enteredText, + enteredWordPos); + researchLogger.mStatistics.recordGestureInput(enteredText.length()); } - private static final String[] EVENTKEYS_STATISTICS = { - "Statistics", "charCount", "letterCount", "numberCount", "spaceCount", "deleteOpsCount", - "wordCount", "isEmptyUponStarting", "isEmptinessStateKnown", "averageTimeBetweenKeys", - "averageTimeBeforeDelete", "averageTimeDuringRepeatedDelete", "averageTimeAfterDelete" - }; + /** + * Log a call to LatinIME.handleBackspace(). + * + * 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"); + public static void latinIME_handleBackspace_batch(final CharSequence deletedText) { + final ResearchLogger researchLogger = getInstance(); + researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_HANDLEBACKSPACE_BATCH, deletedText); + researchLogger.mStatistics.recordGestureDelete(); + } + + /** + * 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"); 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); } } diff --git a/java/src/com/android/inputmethod/research/Statistics.java b/java/src/com/android/inputmethod/research/Statistics.java index eab465aa2..e9c02c919 100644 --- a/java/src/com/android/inputmethod/research/Statistics.java +++ b/java/src/com/android/inputmethod/research/Statistics.java @@ -16,9 +16,15 @@ 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; + // Number of characters entered during a typing session int mCharCount; // Number of letter characters entered during a typing session @@ -31,6 +37,20 @@ 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 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 a commit was reverted in this session. + int mRevertCommitsCount; // Whether the text field was empty upon editing boolean mIsEmptyUponStarting; boolean mIsEmptinessStateKnown; @@ -91,20 +111,31 @@ public class Statistics { mSpaceCount = 0; mDeleteKeyCount = 0; mWordCount = 0; + mDictionaryWordCount = 0; + mSplitWordsCount = 0; + mGesturesInputCount = 0; + mGesturesDeletedCount = 0; + mManualSuggestionsCount = 0; + mRevertCommitsCount = 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) { + if (DEBUG) { + Log.d(TAG, "recordChar() called"); + } final long delta = time - mLastTapTime; - if (codePoint == Keyboard.CODE_DELETE) { + if (codePoint == Constants.CODE_DELETE) { mDeleteKeyCount++; if (delta < MIN_DELETION_INTERMISSION) { if (mIsLastKeyDeleteKey) { @@ -135,12 +166,35 @@ public class Statistics { mLastTapTime = time; } - public void recordWordEntered() { + public void recordWordEntered(final boolean isDictionaryWord) { mWordCount++; + if (isDictionaryWord) { + mDictionaryWordCount++; + } + } + + public void recordSplitWords() { + mSplitWordsCount++; + } + + public void recordGestureInput(final int numCharsEntered) { + mGesturesInputCount++; + mGesturesCharsCount += numCharsEntered; } public void setIsEmptyUponStarting(final boolean isEmpty) { mIsEmptyUponStarting = isEmpty; mIsEmptinessStateKnown = true; } + + public void recordGestureDelete() { + mGesturesDeletedCount++; + } + public void recordManualSuggestion() { + mManualSuggestionsCount++; + } + + public void recordRevertCommit() { + mRevertCommitsCount++; + } } diff --git a/java/src/com/android/inputmethod/research/UploaderService.java b/java/src/com/android/inputmethod/research/UploaderService.java index 7a5749096..a1ecc1118 100644 --- a/java/src/com/android/inputmethod/research/UploaderService.java +++ b/java/src/com/android/inputmethod/research/UploaderService.java @@ -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,6 +46,10 @@ 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() + ".extra.UPLOAD_UNCONDITIONALLY"; @@ -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) { @@ -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 { |