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/FeedbackActivity.java1
-rw-r--r--java/src/com/android/inputmethod/research/FeedbackFragment.java1
-rw-r--r--java/src/com/android/inputmethod/research/FeedbackLog.java32
-rw-r--r--java/src/com/android/inputmethod/research/JsonUtils.java9
-rw-r--r--java/src/com/android/inputmethod/research/LogUnit.java40
-rw-r--r--java/src/com/android/inputmethod/research/MainLogBuffer.java72
-rw-r--r--java/src/com/android/inputmethod/research/MotionEventReader.java10
-rw-r--r--java/src/com/android/inputmethod/research/ResearchLog.java14
-rw-r--r--java/src/com/android/inputmethod/research/ResearchLogger.java444
-rw-r--r--java/src/com/android/inputmethod/research/Statistics.java50
-rw-r--r--java/src/com/android/inputmethod/research/Uploader.java4
-rw-r--r--java/src/com/android/inputmethod/research/UploaderService.java5
-rw-r--r--java/src/com/android/inputmethod/research/ui/SplashScreen.java111
13 files changed, 548 insertions, 245 deletions
diff --git a/java/src/com/android/inputmethod/research/FeedbackActivity.java b/java/src/com/android/inputmethod/research/FeedbackActivity.java
index b985fda21..520b88d2f 100644
--- a/java/src/com/android/inputmethod/research/FeedbackActivity.java
+++ b/java/src/com/android/inputmethod/research/FeedbackActivity.java
@@ -18,7 +18,6 @@ package com.android.inputmethod.research;
import android.app.Activity;
import android.os.Bundle;
-import android.widget.CheckBox;
import com.android.inputmethod.latin.R;
diff --git a/java/src/com/android/inputmethod/research/FeedbackFragment.java b/java/src/com/android/inputmethod/research/FeedbackFragment.java
index a0738292e..75fbbf1ba 100644
--- a/java/src/com/android/inputmethod/research/FeedbackFragment.java
+++ b/java/src/com/android/inputmethod/research/FeedbackFragment.java
@@ -16,7 +16,6 @@
package com.android.inputmethod.research;
-import android.app.Activity;
import android.app.Fragment;
import android.os.Bundle;
import android.text.Editable;
diff --git a/java/src/com/android/inputmethod/research/FeedbackLog.java b/java/src/com/android/inputmethod/research/FeedbackLog.java
new file mode 100644
index 000000000..5af194c32
--- /dev/null
+++ b/java/src/com/android/inputmethod/research/FeedbackLog.java
@@ -0,0 +1,32 @@
+/*
+ * 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.Context;
+
+import java.io.File;
+
+public class FeedbackLog extends ResearchLog {
+ public FeedbackLog(final File outputFile, final Context context) {
+ super(outputFile, context);
+ }
+
+ @Override
+ public boolean isFeedbackLog() {
+ return true;
+ }
+}
diff --git a/java/src/com/android/inputmethod/research/JsonUtils.java b/java/src/com/android/inputmethod/research/JsonUtils.java
index 24cd8d935..63d524df7 100644
--- a/java/src/com/android/inputmethod/research/JsonUtils.java
+++ b/java/src/com/android/inputmethod/research/JsonUtils.java
@@ -94,12 +94,17 @@ import java.util.Map;
.value(words.mIsPunctuationSuggestions);
jsonWriter.name("isObsoleteSuggestions").value(words.mIsObsoleteSuggestions);
jsonWriter.name("isPrediction").value(words.mIsPrediction);
- jsonWriter.name("words");
+ jsonWriter.name("suggestedWords");
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.beginObject();
+ jsonWriter.name("word").value(wordInfo.toString());
+ jsonWriter.name("score").value(wordInfo.mScore);
+ jsonWriter.name("kind").value(wordInfo.mKind);
+ jsonWriter.name("sourceDict").value(wordInfo.mSourceDict);
+ jsonWriter.endObject();
}
jsonWriter.endArray();
jsonWriter.endObject();
diff --git a/java/src/com/android/inputmethod/research/LogUnit.java b/java/src/com/android/inputmethod/research/LogUnit.java
index cf1388f46..3366df12a 100644
--- a/java/src/com/android/inputmethod/research/LogUnit.java
+++ b/java/src/com/android/inputmethod/research/LogUnit.java
@@ -67,7 +67,7 @@ public class LogUnit {
private String[] mWordArray = EMPTY_STRING_ARRAY;
private boolean mMayContainDigit;
private boolean mIsPartOfMegaword;
- private boolean mContainsCorrection;
+ private boolean mContainsUserDeletions;
// mCorrectionType indicates whether the word was corrected at all, and if so, the nature of the
// correction.
@@ -146,7 +146,8 @@ public class LogUnit {
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;
+ JsonWriter jsonWriter = researchLog.getInitializedJsonWriterLocked();
+ outputLogUnitStart(jsonWriter, canIncludePrivateData);
for (int i = 0; i < size; i++) {
final LogStatement logStatement = mLogStatementList.get(i);
if (!canIncludePrivateData && logStatement.isPotentiallyPrivate()) {
@@ -155,42 +156,35 @@ public class LogUnit {
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 (jsonWriter != null) {
- // We must have called logUnitStart earlier, so emit a logUnitStop.
- outputLogUnitStop(jsonWriter);
- }
+ outputLogUnitStop(jsonWriter);
}
}
private static final String WORD_KEY = "_wo";
+ private static final String NUM_WORDS_KEY = "_nw";
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);
+ false /* isPotentiallyRevealing */, WORD_KEY, CORRECTION_TYPE_KEY,
+ NUM_WORDS_KEY);
final LogStatement LOGSTATEMENT_LOG_UNIT_BEGIN_WITHOUT_PRIVATE_DATA =
new LogStatement(LOG_UNIT_BEGIN_KEY, false /* isPotentiallyPrivate */,
- false /* isPotentiallyRevealing */);
+ false /* isPotentiallyRevealing */, NUM_WORDS_KEY);
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(), getWordsAsString(), getCorrectionType());
+ SystemClock.uptimeMillis(), getWordsAsString(), getCorrectionType(),
+ getNumWords());
} else {
LOGSTATEMENT_LOG_UNIT_BEGIN_WITHOUT_PRIVATE_DATA.outputToLocked(jsonWriter,
- SystemClock.uptimeMillis());
+ SystemClock.uptimeMillis(), getNumWords());
}
}
@@ -277,13 +271,13 @@ public class LogUnit {
}
// TODO: Refactor to eliminate getter/setters
- public void setContainsCorrection() {
- mContainsCorrection = true;
+ public void setContainsUserDeletions() {
+ mContainsUserDeletions = true;
}
// TODO: Refactor to eliminate getter/setters
- public boolean containsCorrection() {
- return mContainsCorrection;
+ public boolean containsUserDeletions() {
+ return mContainsUserDeletions;
}
// TODO: Refactor to eliminate getter/setters
@@ -323,7 +317,7 @@ public class LogUnit {
true /* isPartOfMegaword */);
newLogUnit.mWords = null;
newLogUnit.mMayContainDigit = mMayContainDigit;
- newLogUnit.mContainsCorrection = mContainsCorrection;
+ newLogUnit.mContainsUserDeletions = mContainsUserDeletions;
// Purge the logStatements and associated data from this LogUnit.
laterLogStatements.clear();
@@ -346,7 +340,7 @@ public class LogUnit {
setWords(logUnit.mWords);
}
mMayContainDigit = mMayContainDigit || logUnit.mMayContainDigit;
- mContainsCorrection = mContainsCorrection || logUnit.mContainsCorrection;
+ mContainsUserDeletions = mContainsUserDeletions || logUnit.mContainsUserDeletions;
mIsPartOfMegaword = false;
}
diff --git a/java/src/com/android/inputmethod/research/MainLogBuffer.java b/java/src/com/android/inputmethod/research/MainLogBuffer.java
index 9aa349906..6df7c1708 100644
--- a/java/src/com/android/inputmethod/research/MainLogBuffer.java
+++ b/java/src/com/android/inputmethod/research/MainLogBuffer.java
@@ -63,6 +63,15 @@ public abstract class MainLogBuffer extends FixedLogBuffer {
private static final boolean DEBUG = false
&& ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG;
+ // Keep consistent with switch statement in Statistics.recordPublishabilityResultCode()
+ public static final int PUBLISHABILITY_PUBLISHABLE = 0;
+ public static final int PUBLISHABILITY_UNPUBLISHABLE_STOPPING = 1;
+ public static final int PUBLISHABILITY_UNPUBLISHABLE_INCORRECT_WORD_COUNT = 2;
+ public static final int PUBLISHABILITY_UNPUBLISHABLE_SAMPLED_TOO_RECENTLY = 3;
+ public static final int PUBLISHABILITY_UNPUBLISHABLE_DICTIONARY_UNAVAILABLE = 4;
+ public static final int PUBLISHABILITY_UNPUBLISHABLE_MAY_CONTAIN_DIGIT = 5;
+ public static final int PUBLISHABILITY_UNPUBLISHABLE_NOT_IN_DICTIONARY = 6;
+
// The size of the n-grams logged. E.g. N_GRAM_SIZE = 2 means to sample bigrams.
public static final int N_GRAM_SIZE = 2;
@@ -105,21 +114,24 @@ public abstract class MainLogBuffer extends FixedLogBuffer {
}
/**
- * Determines whether uploading the n words at the front the MainLogBuffer will not violate
- * user privacy.
+ * Determines whether the string determined by a series of LogUnits will not violate user
+ * privacy if published.
+ *
+ * @param logUnits a LogUnit list to check for publishability
+ * @param nGramSize the smallest n-gram acceptable to be published. if
+ * {@link ResearchLogger#IS_LOGGING_EVERYTHING} is true, then publish if there are more than
+ * {@code minNGramSize} words in the logUnits, otherwise wait. if {@link
+ * ResearchLogger#IS_LOGGING_EVERYTHING} is false, then ensure that there are exactly nGramSize
+ * words in the LogUnits.
*
- * The size of the MainLogBuffer is just enough to hold one n-gram, its corrections, and any
- * non-character data that is typed between words. The decision about privacy is made based on
- * the buffer's entire content. If it is decided that the privacy risks are too great to upload
- * the contents of this buffer, a censored version of the LogItems may still be uploaded. E.g.,
- * the screen orientation and other characteristics about the device can be uploaded without
- * revealing much about the user.
+ * @return one of the {@code PUBLISHABILITY_*} result codes defined in this class.
*/
- private boolean isSafeNGram(final ArrayList<LogUnit> logUnits, final int minNGramSize) {
+ private int getPublishabilityResultCode(final ArrayList<LogUnit> logUnits,
+ final int nGramSize) {
// Bypass privacy checks when debugging.
if (ResearchLogger.IS_LOGGING_EVERYTHING) {
if (mIsStopping) {
- return true;
+ return PUBLISHABILITY_UNPUBLISHABLE_STOPPING;
}
// Only check that it is the right length. If not, wait for later words to make
// complete n-grams.
@@ -129,13 +141,17 @@ public abstract class MainLogBuffer extends FixedLogBuffer {
final LogUnit logUnit = logUnits.get(i);
numWordsInLogUnitList += logUnit.getNumWords();
}
- return numWordsInLogUnitList >= minNGramSize;
+ if (numWordsInLogUnitList >= nGramSize) {
+ return PUBLISHABILITY_PUBLISHABLE;
+ } else {
+ return PUBLISHABILITY_UNPUBLISHABLE_INCORRECT_WORD_COUNT;
+ }
}
// Check that we are not sampling too frequently. Having sampled recently might disclose
// too much of the user's intended meaning.
if (mNumWordsUntilSafeToSample > 0) {
- return false;
+ return PUBLISHABILITY_UNPUBLISHABLE_SAMPLED_TOO_RECENTLY;
}
// Reload the dictionary in case it has changed (e.g., because the user has changed
// languages).
@@ -144,7 +160,7 @@ public abstract class MainLogBuffer extends FixedLogBuffer {
// 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;
+ return PUBLISHABILITY_UNPUBLISHABLE_DICTIONARY_UNAVAILABLE;
}
// Check each word in the buffer. If any word poses a privacy threat, we cannot upload
@@ -155,7 +171,7 @@ public abstract class MainLogBuffer extends FixedLogBuffer {
if (!logUnit.hasOneOrMoreWords()) {
// Digits outside words are a privacy threat.
if (logUnit.mayContainDigit()) {
- return false;
+ return PUBLISHABILITY_UNPUBLISHABLE_MAY_CONTAIN_DIGIT;
}
} else {
numWordsInLogUnitList += logUnit.getNumWords();
@@ -168,14 +184,18 @@ public abstract class MainLogBuffer extends FixedLogBuffer {
+ ResearchLogger.hasLetters(word)
+ ", isValid: " + (dictionary.isValidWord(word)));
}
- return false;
+ return PUBLISHABILITY_UNPUBLISHABLE_NOT_IN_DICTIONARY;
}
}
}
}
// Finally, only return true if the ngram is the right size.
- return numWordsInLogUnitList == minNGramSize;
+ if (numWordsInLogUnitList == nGramSize) {
+ return PUBLISHABILITY_PUBLISHABLE;
+ } else {
+ return PUBLISHABILITY_UNPUBLISHABLE_INCORRECT_WORD_COUNT;
+ }
}
public void shiftAndPublishAll() throws IOException {
@@ -196,11 +216,29 @@ public abstract class MainLogBuffer extends FixedLogBuffer {
}
}
+ /**
+ * If there is a safe n-gram at the front of this log buffer, publish it with all details, and
+ * remove the LogUnits that constitute it.
+ *
+ * An n-gram might not be "safe" if it violates privacy controls. E.g., it might contain
+ * numbers, an out-of-vocabulary word, or another n-gram may have been published recently. If
+ * there is no safe n-gram, then the LogUnits up through the first word-containing LogUnit are
+ * published, but without disclosing any privacy-related details, such as the word the LogUnit
+ * generated, motion data, etc.
+ *
+ * Note that a LogUnit can hold more than one word if the user types without explicit spaces.
+ * In this case, the words may be grouped together in such a way that pulling an n-gram off the
+ * front would require splitting a LogUnit. Splitting a LogUnit is not possible, so this case
+ * is treated just as the unsafe n-gram case. This may cause n-grams to be sampled at slightly
+ * less than the target frequency.
+ */
protected final void publishLogUnitsAtFrontOfBuffer() throws IOException {
// TODO: Refactor this method to require fewer passes through the LogUnits. Should really
// require only one pass.
ArrayList<LogUnit> logUnits = peekAtFirstNWords(N_GRAM_SIZE);
- if (isSafeNGram(logUnits, N_GRAM_SIZE)) {
+ final int publishabilityResultCode = getPublishabilityResultCode(logUnits, N_GRAM_SIZE);
+ ResearchLogger.recordPublishabilityResultCode(publishabilityResultCode);
+ if (publishabilityResultCode == MainLogBuffer.PUBLISHABILITY_PUBLISHABLE) {
// Good n-gram at the front of the buffer. Publish it, disclosing details.
publish(logUnits, true /* canIncludePrivateData */);
shiftOutWords(N_GRAM_SIZE);
diff --git a/java/src/com/android/inputmethod/research/MotionEventReader.java b/java/src/com/android/inputmethod/research/MotionEventReader.java
index fbfd9b531..3388645b7 100644
--- a/java/src/com/android/inputmethod/research/MotionEventReader.java
+++ b/java/src/com/android/inputmethod/research/MotionEventReader.java
@@ -315,16 +315,6 @@ public class MotionEventReader {
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) {
diff --git a/java/src/com/android/inputmethod/research/ResearchLog.java b/java/src/com/android/inputmethod/research/ResearchLog.java
index 3e82139a6..46e620ae5 100644
--- a/java/src/com/android/inputmethod/research/ResearchLog.java
+++ b/java/src/com/android/inputmethod/research/ResearchLog.java
@@ -27,7 +27,6 @@ import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
-import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
@@ -56,7 +55,7 @@ public class ResearchLog {
private static final String TAG = ResearchLog.class.getSimpleName();
private static final boolean DEBUG = false
&& ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG;
- private static final long FLUSH_DELAY_IN_MS = 1000 * 5;
+ private static final long FLUSH_DELAY_IN_MS = TimeUnit.SECONDS.toMillis(5);
/* package */ final ScheduledExecutorService mExecutor;
/* package */ final File mFile;
@@ -81,6 +80,17 @@ public class ResearchLog {
}
/**
+ * Returns true if this is a FeedbackLog.
+ *
+ * FeedbackLogs record only the data associated with a Feedback dialog. Instead of normal
+ * logging, they contain a LogStatement with the complete feedback string and optionally a
+ * recording of the user's supplied demo of the problem.
+ */
+ public boolean isFeedbackLog() {
+ return false;
+ }
+
+ /**
* Waits for any publication requests to finish and closes the {@link JsonWriter} used for
* output.
*
diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java
index 8b8ea21e9..25187ced1 100644
--- a/java/src/com/android/inputmethod/research/ResearchLogger.java
+++ b/java/src/com/android/inputmethod/research/ResearchLogger.java
@@ -20,11 +20,7 @@ import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.KEYBOAR
import android.accounts.Account;
import android.accounts.AccountManager;
-import android.app.AlertDialog;
-import android.app.Dialog;
import android.content.Context;
-import android.content.DialogInterface;
-import android.content.DialogInterface.OnCancelListener;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
@@ -34,7 +30,6 @@ import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
-import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
@@ -42,12 +37,9 @@ 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;
import android.view.KeyEvent;
import android.view.MotionEvent;
-import android.view.Window;
-import android.view.WindowManager;
import android.view.inputmethod.CompletionInfo;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
@@ -61,15 +53,16 @@ import com.android.inputmethod.keyboard.KeyboardView;
import com.android.inputmethod.keyboard.MainKeyboardView;
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;
-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.latin.utils.InputTypeUtils;
+import com.android.inputmethod.latin.utils.TextRange;
import com.android.inputmethod.research.MotionEventReader.ReplayData;
+import com.android.inputmethod.research.ui.SplashScreen;
import java.io.File;
import java.io.FileInputStream;
@@ -81,8 +74,11 @@ import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
+import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
+// TODO: Add a unit test for every "logging" method (i.e. that is called from the IME and calls
+// enqueueEvent to record a LogStatement).
/**
* Logs the use of the LatinIME keyboard.
*
@@ -92,12 +88,12 @@ import java.util.regex.Pattern;
* This functionality is off by default. See
* {@link ProductionFlag#USES_DEVELOPMENT_ONLY_DIAGNOSTICS}.
*/
-public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChangeListener {
+public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChangeListener,
+ SplashScreen.UserConsentListener {
// 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.
@@ -141,10 +137,10 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
private static final int MAX_INPUTVIEW_LENGTH_TO_CAPTURE = 8192; // must be >=1
private static final String PREF_RESEARCH_SAVED_CHANNEL = "pref_research_saved_channel";
- private static final long RESEARCHLOG_CLOSE_TIMEOUT_IN_MS = 5 * 1000;
- private static final long RESEARCHLOG_ABORT_TIMEOUT_IN_MS = 5 * 1000;
- private static final long DURATION_BETWEEN_DIR_CLEANUP_IN_MS = DateUtils.DAY_IN_MILLIS;
- private static final long MAX_LOGFILE_AGE_IN_MS = 4 * DateUtils.DAY_IN_MILLIS;
+ private static final long RESEARCHLOG_CLOSE_TIMEOUT_IN_MS = TimeUnit.SECONDS.toMillis(5);
+ private static final long RESEARCHLOG_ABORT_TIMEOUT_IN_MS = TimeUnit.SECONDS.toMillis(5);
+ private static final long DURATION_BETWEEN_DIR_CLEANUP_IN_MS = TimeUnit.DAYS.toMillis(1);
+ private static final long MAX_LOGFILE_AGE_IN_MS = TimeUnit.DAYS.toMillis(4);
private static final ResearchLogger sInstance = new ResearchLogger();
private static String sAccountType = null;
@@ -182,8 +178,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
private final MotionEventReader mMotionEventReader = new MotionEventReader();
private final Replayer mReplayer = Replayer.getInstance();
private ResearchLogDirectory mResearchLogDirectory;
+ private SplashScreen mSplashScreen;
- private Intent mUploadIntent;
private Intent mUploadNowIntent;
/* package for test */ LogUnit mCurrentLogUnit = new LogUnit();
@@ -194,9 +190,16 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
// gesture, and when committing the earlier word, split the LogUnit.
private long mSavedDownEventTime;
private Bundle mFeedbackDialogBundle = null;
+ // Whether the feedback dialog is visible, and the user is typing into it. Normal logging is
+ // not performed on text that the user types into the feedback dialog.
private boolean mInFeedbackDialog = false;
private Handler mUserRecordingTimeoutHandler;
- private static final long USER_RECORDING_TIMEOUT_MS = 30L * DateUtils.SECOND_IN_MILLIS;
+ private static final long USER_RECORDING_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(30);
+
+ // Stores a temporary LogUnit while generating a phantom space. Needed because phantom spaces
+ // are issued out-of-order, immediately before the characters generated by other operations that
+ // have already outputted LogStatements.
+ private LogUnit mPhantomSpaceLogUnit = null;
private ResearchLogger() {
mStatistics = Statistics.getInstance();
@@ -229,7 +232,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
resetLogBuffers();
// Initialize external services
- mUploadIntent = new Intent(mLatinIME, UploaderService.class);
mUploadNowIntent = new Intent(mLatinIME, UploaderService.class);
mUploadNowIntent.putExtra(UploaderService.EXTRA_UPLOAD_UNCONDITIONALLY, true);
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
@@ -253,14 +255,14 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
if (DEBUG) {
final String wordsString = logUnit.getWordsAsString();
Log.d(TAG, "onPublish: '" + wordsString
- + "', hc: " + logUnit.containsCorrection()
+ + "', hc: " + logUnit.containsUserDeletions()
+ ", cipd: " + canIncludePrivateData);
}
for (final String word : logUnit.getWordsAsStringArray()) {
final Dictionary dictionary = getDictionary();
mStatistics.recordWordEntered(
dictionary != null && dictionary.isValidWord(word),
- logUnit.containsCorrection());
+ logUnit.containsUserDeletions());
}
}
publishLogUnits(logUnits, mMainResearchLog, canIncludePrivateData);
@@ -292,62 +294,19 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
}
}
- private Dialog mSplashDialog = null;
-
private void maybeShowSplashScreen() {
- if (ResearchSettings.readHasSeenSplash(mPrefs)) {
- return;
- }
- if (mSplashDialog != null && mSplashDialog.isShowing()) {
- return;
- }
- final IBinder windowToken = mMainKeyboardView != null
- ? mMainKeyboardView.getWindowToken() : null;
- if (windowToken == null) {
- return;
- }
- 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,
- new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- onUserLoggingConsent();
- mSplashDialog.dismiss();
- }
- })
- .setNegativeButton(android.R.string.no,
- new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- 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);
- mLatinIME.startActivity(intent);
- }
- })
- .setCancelable(true)
- .setOnCancelListener(
- new OnCancelListener() {
- @Override
- public void onCancel(DialogInterface dialog) {
- mLatinIME.requestHideSelf(0);
- }
- });
- mSplashDialog = builder.create();
- final Window w = mSplashDialog.getWindow();
- final WindowManager.LayoutParams lp = w.getAttributes();
- lp.token = windowToken;
- lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
- w.setAttributes(lp);
- w.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
- mSplashDialog.show();
- }
-
- public void onUserLoggingConsent() {
+ if (ResearchSettings.readHasSeenSplash(mPrefs)) return;
+ if (mSplashScreen != null && mSplashScreen.isShowing()) return;
+ if (mMainKeyboardView == null) return;
+ final IBinder windowToken = mMainKeyboardView.getWindowToken();
+ if (windowToken == null) return;
+
+ mSplashScreen = new SplashScreen(mLatinIME, this);
+ mSplashScreen.showSplashScreen(windowToken);
+ }
+
+ @Override
+ public void onSplashScreenUserClickedOk() {
if (mPrefs == null) {
mPrefs = PreferenceManager.getDefaultSharedPreferences(mLatinIME);
if (mPrefs == null) return;
@@ -358,12 +317,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
restart();
}
- private void setLoggingAllowed(final boolean enableLogging) {
- if (mPrefs == null) return;
- sIsLogging = enableLogging;
- ResearchSettings.writeResearchLoggerEnabledFlag(mPrefs, enableLogging);
- }
-
private void checkForEmptyEditor() {
if (mLatinIME == null) {
return;
@@ -420,6 +373,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
mMainResearchLog.blockingClose(RESEARCHLOG_CLOSE_TIMEOUT_IN_MS);
resetLogBuffers();
+ cancelFeedbackDialog();
}
public void abort() {
@@ -456,6 +410,12 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
presentFeedbackDialog(latinIME);
}
+ public void presentFeedbackDialogFromSettings() {
+ if (mLatinIME != null) {
+ presentFeedbackDialog(mLatinIME);
+ }
+ }
+
public void presentFeedbackDialog(final LatinIME latinIME) {
if (isMakingUserRecording()) {
saveRecording();
@@ -574,8 +534,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
toast.show();
boolean isLogDeleted = abort();
final long currentTime = System.currentTimeMillis();
- final long resumeTime = currentTime + 1000 * 60 *
- SUSPEND_DURATION_IN_MINUTES;
+ final long resumeTime = currentTime
+ + TimeUnit.MINUTES.toMillis(SUSPEND_DURATION_IN_MINUTES);
suspendLoggingUntil(resumeTime);
toast.cancel();
Toast.makeText(latinIME, R.string.research_notify_logging_suspended,
@@ -650,7 +610,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
feedbackLogUnit.addLogStatement(LOGSTATEMENT_FEEDBACK, SystemClock.uptimeMillis(),
feedbackContents, accountName, recording);
- final ResearchLog feedbackLog = new ResearchLog(mResearchLogDirectory.getLogFilePath(
+ final ResearchLog feedbackLog = new FeedbackLog(mResearchLogDirectory.getLogFilePath(
System.currentTimeMillis(), System.nanoTime()), mLatinIME);
final LogBuffer feedbackLogBuffer = new LogBuffer();
feedbackLogBuffer.shiftIn(feedbackLogUnit);
@@ -667,7 +627,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
mMotionEventReader.readMotionEventData(mUserRecordingFile);
mReplayer.replay(replayData, null);
}
- }, 1000);
+ }, TimeUnit.SECONDS.toMillis(1));
}
if (FEEDBACK_DIALOG_SHOULD_PRESERVE_TEXT_FIELD) {
@@ -692,13 +652,19 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
mInFeedbackDialog = false;
}
+ private void cancelFeedbackDialog() {
+ if (isMakingUserRecording()) {
+ cancelRecording();
+ }
+ mInFeedbackDialog = false;
+ }
+
public void initSuggest(final Suggest suggest) {
mSuggest = suggest;
// MainLogBuffer now has an out-of-date Suggest object. Close down MainLogBuffer and create
// a new one.
if (mMainLogBuffer != null) {
- stop();
- start();
+ restart();
}
}
@@ -713,8 +679,28 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
mIsPasswordView = isPasswordView;
}
- private boolean isAllowedToLog() {
- return !mIsPasswordView && sIsLogging && !mInFeedbackDialog;
+ /**
+ * Returns true if logging is permitted.
+ *
+ * This method is called when adding a LogStatement to a LogUnit, and when adding a LogUnit to a
+ * ResearchLog. It is checked in both places in case conditions change between these times, and
+ * as a defensive measure in case refactoring changes the logging pipeline.
+ */
+ private boolean isAllowedToLogTo(final ResearchLog researchLog) {
+ // Logging is never allowed in these circumstances
+ if (mIsPasswordView) return false;
+ if (!sIsLogging) return false;
+ if (mInFeedbackDialog) {
+ // The FeedbackDialog is up. Normal logging should not happen (the user might be trying
+ // out things while the dialog is up, and their reporting of an issue may not be
+ // representative of what they normally type). However, after the user has finished
+ // entering their feedback, the logger packs their comments and an encoded version of
+ // any demonstration of the issue into a special "FeedbackLog". So if the FeedbackLog
+ // is the destination, we do want to allow logging to it.
+ return researchLog.isFeedbackLog();
+ }
+ // No other exclusions. Logging is permitted.
+ return true;
}
public void requestIndicatorRedraw() {
@@ -747,7 +733,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
// and remove this method.
// The check for MainKeyboardView ensures that the indicator only decorates the main
// keyboard, not every keyboard.
- if (IS_SHOWING_INDICATOR && (isAllowedToLog() || isReplaying())
+ if (IS_SHOWING_INDICATOR && (isAllowedToLogTo(mMainResearchLog) || isReplaying())
&& view instanceof MainKeyboardView) {
final int savedColor = paint.getColor();
paint.setColor(getIndicatorColor());
@@ -782,7 +768,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
private synchronized void enqueueEvent(final LogUnit logUnit, final LogStatement logStatement,
final Object... values) {
assert values.length == logStatement.getKeys().length;
- if (isAllowedToLog() && logUnit != null) {
+ if (isAllowedToLogTo(mMainResearchLog) && logUnit != null) {
final long time = SystemClock.uptimeMillis();
logUnit.addLogStatement(logStatement, time, values);
}
@@ -792,8 +778,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
mCurrentLogUnit.setMayContainDigit();
}
- private void setCurrentLogUnitContainsCorrection() {
- mCurrentLogUnit.setContainsCorrection();
+ private void setCurrentLogUnitContainsUserDeletions() {
+ mCurrentLogUnit.setContainsUserDeletions();
}
private void setCurrentLogUnitCorrectionType(final int correctionType) {
@@ -825,20 +811,22 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
// 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)
+ // it the 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.
final LogUnit oldLogUnit = mMainLogBuffer.peekLastLogUnit();
- // Check that expected word matches.
+ // Check that expected word matches. It's ok if both strings are null, because this is the
+ // case where the LogUnit is storing a non-word, e.g. a separator.
if (oldLogUnit != null) {
+ // Because the word is stored in the LogUnit with digits scrubbed, the comparison must
+ // be made on a scrubbed version of the expectedWord as well.
+ final String scrubbedExpectedWord = scrubDigitsFromString(expectedWord);
final String oldLogUnitWords = oldLogUnit.getWordsAsString();
- if (oldLogUnitWords != null && !oldLogUnitWords.equals(expectedWord)) {
- return;
- }
+ if (!TextUtils.equals(scrubbedExpectedWord, oldLogUnitWords)) return;
}
// Uncommit, merging if necessary.
@@ -881,7 +869,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
final ResearchLog researchLog, final boolean canIncludePrivateData) {
final LogUnit openingLogUnit = new LogUnit();
if (logUnits.isEmpty()) return;
- if (!isAllowedToLog()) return;
+ if (!isAllowedToLogTo(researchLog)) return;
// LogUnits not containing private data, such as contextual data for the log, do not require
// logSegment boundary statements.
if (canIncludePrivateData) {
@@ -893,7 +881,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
if (DEBUG) {
Log.d(TAG, "publishLogBuffer: " + (logUnit.hasOneOrMoreWords()
? logUnit.getWordsAsString() : "<wordless>")
- + ", correction?: " + logUnit.containsCorrection());
+ + ", correction?: " + logUnit.containsUserDeletions());
}
researchLog.publish(logUnit, canIncludePrivateData);
}
@@ -954,7 +942,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
return Character.isDigit(codePoint) ? DIGIT_REPLACEMENT_CODEPOINT : codePoint;
}
- /* package for test */ static String scrubDigitsFromString(String s) {
+ /* package for test */ static String scrubDigitsFromString(final String s) {
+ if (s == null) return null;
StringBuilder sb = null;
final int length = s.length();
for (int i = 0; i < length; i = s.offsetByCodePoints(i, 1)) {
@@ -1077,22 +1066,24 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
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 = 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();
- }
+ public static void mainKeyboardView_processMotionEvent(final MotionEvent me) {
+ if (me == null) {
+ return;
+ }
+ final int action = me.getActionMasked();
+ final long eventTime = me.getEventTime();
+ 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();
}
}
@@ -1223,7 +1214,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
final RichInputConnection connection) {
String word = "";
if (connection != null) {
- Range range = connection.getWordRangeAtCursor(WHITESPACE_SEPARATORS, 1);
+ TextRange range = connection.getWordRangeAtCursor(WHITESPACE_SEPARATORS, 1);
if (range != null) {
word = range.mWord.toString();
}
@@ -1247,26 +1238,50 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
}
/**
+ * Log a revert of onTextInput() (known in the IME as "EnteredText").
+ *
+ * SystemResponse: Remove the LogUnit recording the textInput
+ */
+ public static void latinIME_handleBackspace_cancelTextInput(final String text) {
+ final ResearchLogger researchLogger = getInstance();
+ researchLogger.uncommitCurrentLogUnit(text, true /* dumpCurrentLogUnit */);
+ }
+
+ /**
* 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");
+ "suggestion", "x", "y", "isBatchMode", "score", "kind", "sourceDict");
+ /**
+ * Log a call to LatinIME.pickSuggestionManually().
+ *
+ * @param replacedWord the typed word that this manual suggestion replaces. May not be null.
+ * @param index the index in the suggestion strip
+ * @param suggestion the committed suggestion. May not be null.
+ * @param isBatchMode whether this was input in batch mode, aka gesture.
+ * @param score the internal score of the suggestion, as output by the dictionary
+ * @param kind the kind of suggestion, as one of the SuggestedWordInfo#KIND_* constants
+ * @param sourceDict the source origin of this word, as one of the Dictionary#TYPE_* constants.
+ */
public static void latinIME_pickSuggestionManually(final String replacedWord,
- final int index, final String suggestion, final boolean isBatchMode) {
+ final int index, final String suggestion, final boolean isBatchMode,
+ final int score, final int kind, final String sourceDict) {
final ResearchLogger researchLogger = getInstance();
+ // Note : suggestion can't be null here, because it's only called in a place where it
+ // can't be null.
if (!replacedWord.equals(suggestion.toString())) {
// The user chose something other than what was already there.
- researchLogger.setCurrentLogUnitContainsCorrection();
+ researchLogger.setCurrentLogUnitContainsUserDeletions();
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);
+ scrubbedWord, Constants.SUGGESTION_STRIP_COORDINATE,
+ Constants.SUGGESTION_STRIP_COORDINATE, isBatchMode, score, kind, sourceDict);
researchLogger.commitCurrentLogUnitAsWord(scrubbedWord, Long.MAX_VALUE, isBatchMode);
researchLogger.mStatistics.recordManualSuggestion(SystemClock.uptimeMillis());
}
@@ -1291,17 +1306,32 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
/**
* Log a call to LatinIME.sendKeyCodePoint().
*
- * SystemResponse: The IME is inserting text into the TextView for numbers, fixed strings, or
- * some other unusual mechanism.
+ * SystemResponse: The IME is inserting text into the TextView for non-word-constituent,
+ * strings (separators, numbers, other symbols).
*/
private static final LogStatement LOGSTATEMENT_LATINIME_SENDKEYCODEPOINT =
new LogStatement("LatinIMESendKeyCodePoint", true, false, "code");
public static void latinIME_sendKeyCodePoint(final int code) {
final ResearchLogger researchLogger = getInstance();
- researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_SENDKEYCODEPOINT,
- Constants.printableCode(scrubDigitFromCodePoint(code)));
- if (Character.isDigit(code)) {
- researchLogger.setCurrentLogUnitContainsDigitFlag();
+ final LogUnit phantomSpaceLogUnit = researchLogger.mPhantomSpaceLogUnit;
+ if (phantomSpaceLogUnit == null) {
+ researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_SENDKEYCODEPOINT,
+ Constants.printableCode(scrubDigitFromCodePoint(code)));
+ if (Character.isDigit(code)) {
+ researchLogger.setCurrentLogUnitContainsDigitFlag();
+ }
+ researchLogger.commitCurrentLogUnit();
+ } else {
+ researchLogger.enqueueEvent(phantomSpaceLogUnit, LOGSTATEMENT_LATINIME_SENDKEYCODEPOINT,
+ Constants.printableCode(scrubDigitFromCodePoint(code)));
+ if (Character.isDigit(code)) {
+ phantomSpaceLogUnit.setMayContainDigit();
+ }
+ researchLogger.mMainLogBuffer.shiftIn(phantomSpaceLogUnit);
+ if (researchLogger.mUserRecordingLogBuffer != null) {
+ researchLogger.mUserRecordingLogBuffer.shiftIn(phantomSpaceLogUnit);
+ }
+ researchLogger.mPhantomSpaceLogUnit = null;
}
}
@@ -1311,12 +1341,18 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
* 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);
+ new LogStatement("LatinIMEPromotePhantomSpace", false, false);
public static void latinIME_promotePhantomSpace() {
+ // A phantom space is always added before the text that triggered it. The triggering text
+ // and the events that created it will be in mCurrentLogUnit, but the phantom space should
+ // be in its own LogUnit, committed before the triggering text. Although it is created
+ // here, it is not added to the LogBuffer until the following call to
+ // latinIME_sendKeyCodePoint, because SENDKEYCODEPOINT LogStatement also must go into that
+ // LogUnit.
final ResearchLogger researchLogger = getInstance();
- final LogUnit logUnit;
- logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit();
- researchLogger.enqueueEvent(logUnit, LOGSTATEMENT_LATINIME_PROMOTEPHANTOMSPACE);
+ researchLogger.mPhantomSpaceLogUnit = new LogUnit();
+ researchLogger.enqueueEvent(researchLogger.mPhantomSpaceLogUnit,
+ LOGSTATEMENT_LATINIME_PROMOTEPHANTOMSPACE);
}
/**
@@ -1402,23 +1438,40 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
public static void latinIME_revertCommit(final String committedWord,
final String originallyTypedWord, final boolean isBatchMode,
final String separatorString) {
+ // TODO: Prioritize adding a unit test for this method (as it is especially complex)
+ // TODO: Update the UserRecording LogBuffer as well as the MainLogBuffer
final ResearchLogger researchLogger = getInstance();
- // TODO: Verify that mCurrentLogUnit has been restored and contains the reverted word.
- final LogUnit logUnit;
- logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit();
- if (originallyTypedWord.length() > 0 && hasLetters(originallyTypedWord)) {
- if (logUnit != null) {
- logUnit.setWords(originallyTypedWord);
- }
- }
- researchLogger.enqueueEvent(logUnit != null ? logUnit : researchLogger.mCurrentLogUnit,
- LOGSTATEMENT_LATINIME_REVERTCOMMIT, committedWord, originallyTypedWord,
- separatorString);
- if (logUnit != null) {
- logUnit.setContainsCorrection();
+ //
+ // 1. Remove separator LogUnit
+ final LogUnit lastLogUnit = researchLogger.mMainLogBuffer.peekLastLogUnit();
+ // Check that we're not at the beginning of input
+ if (lastLogUnit == null) return;
+ // Check that we're after a separator
+ if (lastLogUnit.getWordsAsString() != null) return;
+ // Remove separator
+ final LogUnit separatorLogUnit = researchLogger.mMainLogBuffer.unshiftIn();
+
+ // 2. Add revert LogStatement
+ final LogUnit revertedLogUnit = researchLogger.mMainLogBuffer.peekLastLogUnit();
+ if (revertedLogUnit == null) return;
+ if (!revertedLogUnit.getWordsAsString().equals(scrubDigitsFromString(committedWord))) {
+ // Any word associated with the reverted LogUnit has already had its digits scrubbed, so
+ // any digits in the committedWord argument must also be scrubbed for an accurate
+ // comparison.
+ return;
}
+ researchLogger.enqueueEvent(revertedLogUnit, LOGSTATEMENT_LATINIME_REVERTCOMMIT,
+ committedWord, originallyTypedWord, separatorString);
+
+ // 3. Update the word associated with the LogUnit
+ revertedLogUnit.setWords(originallyTypedWord);
+ revertedLogUnit.setContainsUserDeletions();
+
+ // 4. Re-add the separator LogUnit
+ researchLogger.mMainLogBuffer.shiftIn(separatorLogUnit);
+
+ // 5. Record stats
researchLogger.mStatistics.recordRevertCommit(SystemClock.uptimeMillis());
- researchLogger.commitCurrentLogUnitAsWord(originallyTypedWord, Long.MAX_VALUE, isBatchMode);
}
/**
@@ -1528,7 +1581,12 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_REVERTDOUBLESPACEPERIOD =
new LogStatement("RichInputConnectionRevertDoubleSpacePeriod", false, false);
public static void richInputConnection_revertDoubleSpacePeriod() {
- getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_REVERTDOUBLESPACEPERIOD);
+ final ResearchLogger researchLogger = getInstance();
+ // An extra LogUnit is added for the period; this is removed here because of the revert.
+ researchLogger.uncommitCurrentLogUnit(null, true /* dumpCurrentLogUnit */);
+ // TODO: This will probably be lost as the user backspaces further. Figure out how to put
+ // it into the right logUnit.
+ researchLogger.enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_REVERTDOUBLESPACEPERIOD);
}
/**
@@ -1571,25 +1629,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
}
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();
- final String scrubbedWord = scrubDigitsFromString(committedWord);
- researchLogger.enqueueEvent(LOGSTATEMENT_COMMIT_PARTIAL_TEXT);
- researchLogger.mStatistics.recordAutoCorrection(SystemClock.uptimeMillis());
- researchLogger.commitCurrentLogUnitAsWord(scrubbedWord, lastTimestampOfWordData,
- isBatchMode);
- }
/**
* Log a call to RichInputConnection.commitText().
@@ -1613,12 +1652,24 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
}
/**
- * Shared event for logging committed text.
+ * Shared events for logging committed text.
+ *
+ * The "CommitTextEventHappened" LogStatement is written to the log even if privacy rules
+ * indicate that the word contents should not be logged. It has no contents, and only serves to
+ * record the event and thereby make it easier to calculate word-level statistics even when the
+ * word contents are unknown.
*/
private static final LogStatement LOGSTATEMENT_COMMITTEXT =
- new LogStatement("CommitText", true, false, "committedText", "isBatchMode");
+ new LogStatement("CommitText", true /* isPotentiallyPrivate */,
+ false /* isPotentiallyRevealing */, "committedText", "isBatchMode");
+ private static final LogStatement LOGSTATEMENT_COMMITTEXT_EVENT_HAPPENED =
+ new LogStatement("CommitTextEventHappened", false /* isPotentiallyPrivate */,
+ false /* isPotentiallyRevealing */);
private void enqueueCommitText(final String word, final boolean isBatchMode) {
+ // Event containing the word; will be published only if privacy checks pass
enqueueEvent(LOGSTATEMENT_COMMITTEXT, word, isBatchMode);
+ // Event not containing the word; will always be published
+ enqueueEvent(LOGSTATEMENT_COMMITTEXT_EVENT_HAPPENED);
}
/**
@@ -1716,7 +1767,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
public static void suddenJumpingTouchEventHandler_onTouchEvent(final MotionEvent me) {
if (me != null) {
getInstance().enqueueEvent(LOGSTATEMENT_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT,
- me.toString());
+ MotionEvent.obtain(me));
}
}
@@ -1752,7 +1803,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
*/
private static final LogStatement LOGSTATEMENT_LATINIME_ONENDBATCHINPUT =
new LogStatement("LatinIMEOnEndBatchInput", true, false, "enteredText",
- "enteredWordPos");
+ "enteredWordPos", "suggestedWords");
public static void latinIME_onEndBatchInput(final CharSequence enteredText,
final int enteredWordPos, final SuggestedWords suggestedWords) {
final ResearchLogger researchLogger = getInstance();
@@ -1760,23 +1811,32 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
researchLogger.mCurrentLogUnit.setWords(enteredText.toString());
}
researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_ONENDBATCHINPUT, enteredText,
- enteredWordPos);
+ enteredWordPos, suggestedWords);
researchLogger.mCurrentLogUnit.initializeSuggestions(suggestedWords);
researchLogger.mStatistics.recordGestureInput(enteredText.length(),
SystemClock.uptimeMillis());
}
+ private static final LogStatement LOGSTATEMENT_LATINIME_HANDLEBACKSPACE =
+ new LogStatement("LatinIMEHandleBackspace", true, false, "numCharacters");
/**
* 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.
+ *
+ * @param numCharacters how many characters the backspace operation deleted
+ * @param shouldUncommitLogUnit whether to uncommit the last {@code LogUnit} in the
+ * {@code LogBuffer}
*/
- private static final LogStatement LOGSTATEMENT_LATINIME_HANDLEBACKSPACE =
- new LogStatement("LatinIMEHandleBackspace", true, false, "numCharacters");
- public static void latinIME_handleBackspace(final int numCharacters) {
+ public static void latinIME_handleBackspace(final int numCharacters,
+ final boolean shouldUncommitLogUnit) {
final ResearchLogger researchLogger = getInstance();
researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_HANDLEBACKSPACE, numCharacters);
+ if (shouldUncommitLogUnit) {
+ ResearchLogger.getInstance().uncommitCurrentLogUnit(
+ null, true /* dumpCurrentLogUnit */);
+ }
}
/**
@@ -1794,6 +1854,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
numCharacters);
researchLogger.mStatistics.recordGestureDelete(deletedText.length(),
SystemClock.uptimeMillis());
+ researchLogger.uncommitCurrentLogUnit(deletedText.toString(),
+ false /* dumpCurrentLogUnit */);
}
/**
@@ -1837,6 +1899,20 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
}
/**
+ * Call this method when the logging system has attempted publication of an n-gram.
+ *
+ * Statistics are gathered about the success or failure.
+ *
+ * @param publishabilityResultCode a result code as defined by
+ * {@code MainLogBuffer.PUBLISHABILITY_*}
+ */
+ static void recordPublishabilityResultCode(final int publishabilityResultCode) {
+ final ResearchLogger researchLogger = getInstance();
+ final Statistics statistics = researchLogger.mStatistics;
+ statistics.recordPublishabilityResultCode(publishabilityResultCode);
+ }
+
+ /**
* Log statistics.
*
* ContextualData, recorded at the end of a session.
@@ -1848,7 +1924,11 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
"averageTimeDuringRepeatedDelete", "averageTimeAfterDelete",
"dictionaryWordCount", "splitWordsCount", "gestureInputCount",
"gestureCharsCount", "gesturesDeletedCount", "manualSuggestionsCount",
- "revertCommitsCount", "correctedWordsCount", "autoCorrectionsCount");
+ "revertCommitsCount", "correctedWordsCount", "autoCorrectionsCount",
+ "publishableCount", "unpublishableStoppingCount",
+ "unpublishableIncorrectWordCount", "unpublishableSampledTooRecentlyCount",
+ "unpublishableDictionaryUnavailableCount", "unpublishableMayContainDigitCount",
+ "unpublishableNotInDictionaryCount");
private static void logStatistics() {
final ResearchLogger researchLogger = getInstance();
final Statistics statistics = researchLogger.mStatistics;
@@ -1863,6 +1943,10 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
statistics.mGesturesInputCount, statistics.mGesturesCharsCount,
statistics.mGesturesDeletedCount, statistics.mManualSuggestionsCount,
statistics.mRevertCommitsCount, statistics.mCorrectedWordsCount,
- statistics.mAutoCorrectionsCount);
+ statistics.mAutoCorrectionsCount, statistics.mPublishableCount,
+ statistics.mUnpublishableStoppingCount, statistics.mUnpublishableIncorrectWordCount,
+ statistics.mUnpublishableSampledTooRecently,
+ statistics.mUnpublishableDictionaryUnavailable,
+ statistics.mUnpublishableMayContainDigit, statistics.mUnpublishableNotInDictionary);
}
}
diff --git a/java/src/com/android/inputmethod/research/Statistics.java b/java/src/com/android/inputmethod/research/Statistics.java
index 7f6c851bb..fd323a104 100644
--- a/java/src/com/android/inputmethod/research/Statistics.java
+++ b/java/src/com/android/inputmethod/research/Statistics.java
@@ -21,6 +21,8 @@ import android.util.Log;
import com.android.inputmethod.latin.Constants;
import com.android.inputmethod.latin.define.ProductionFlag;
+import java.util.concurrent.TimeUnit;
+
public class Statistics {
private static final String TAG = Statistics.class.getSimpleName();
private static final boolean DEBUG = false
@@ -61,6 +63,16 @@ public class Statistics {
boolean mIsEmptyUponStarting;
boolean mIsEmptinessStateKnown;
+ // Counts of how often an n-gram is collected or not, and the reasons for the decision.
+ // Keep consistent with publishability result code list in MainLogBuffer
+ int mPublishableCount;
+ int mUnpublishableStoppingCount;
+ int mUnpublishableIncorrectWordCount;
+ int mUnpublishableSampledTooRecently;
+ int mUnpublishableDictionaryUnavailable;
+ int mUnpublishableMayContainDigit;
+ int mUnpublishableNotInDictionary;
+
// Timers to count average time to enter a key, first press a delete key,
// between delete keys, and then to return typing after a delete key.
final AverageTimeCounter mKeyCounter = new AverageTimeCounter();
@@ -92,8 +104,8 @@ public class Statistics {
// To account for the interruptions when the user's attention is directed elsewhere, times
// longer than MIN_TYPING_INTERMISSION are not counted when estimating this statistic.
- public static final int MIN_TYPING_INTERMISSION = 2 * 1000; // in milliseconds
- public static final int MIN_DELETION_INTERMISSION = 10 * 1000; // in milliseconds
+ public static final long MIN_TYPING_INTERMISSION = TimeUnit.SECONDS.toMillis(2);
+ public static final long MIN_DELETION_INTERMISSION = TimeUnit.SECONDS.toMillis(10);
// The last time that a tap was performed
private long mLastTapTime;
@@ -133,6 +145,13 @@ public class Statistics {
mAfterDeleteKeyCounter.reset();
mGesturesCharsCount = 0;
mGesturesDeletedCount = 0;
+ mPublishableCount = 0;
+ mUnpublishableStoppingCount = 0;
+ mUnpublishableIncorrectWordCount = 0;
+ mUnpublishableSampledTooRecently = 0;
+ mUnpublishableDictionaryUnavailable = 0;
+ mUnpublishableMayContainDigit = 0;
+ mUnpublishableNotInDictionary = 0;
mLastTapTime = 0;
mIsLastKeyDeleteKey = false;
@@ -230,4 +249,31 @@ public class Statistics {
mIsLastKeyDeleteKey = isDeletion;
mLastTapTime = time;
}
+
+ public void recordPublishabilityResultCode(final int publishabilityResultCode) {
+ // Keep consistent with publishability result code list in MainLogBuffer
+ switch (publishabilityResultCode) {
+ case MainLogBuffer.PUBLISHABILITY_PUBLISHABLE:
+ mPublishableCount++;
+ break;
+ case MainLogBuffer.PUBLISHABILITY_UNPUBLISHABLE_STOPPING:
+ mUnpublishableStoppingCount++;
+ break;
+ case MainLogBuffer.PUBLISHABILITY_UNPUBLISHABLE_INCORRECT_WORD_COUNT:
+ mUnpublishableIncorrectWordCount++;
+ break;
+ case MainLogBuffer.PUBLISHABILITY_UNPUBLISHABLE_SAMPLED_TOO_RECENTLY:
+ mUnpublishableSampledTooRecently++;
+ break;
+ case MainLogBuffer.PUBLISHABILITY_UNPUBLISHABLE_DICTIONARY_UNAVAILABLE:
+ mUnpublishableDictionaryUnavailable++;
+ break;
+ case MainLogBuffer.PUBLISHABILITY_UNPUBLISHABLE_MAY_CONTAIN_DIGIT:
+ mUnpublishableMayContainDigit++;
+ break;
+ case MainLogBuffer.PUBLISHABILITY_UNPUBLISHABLE_NOT_IN_DICTIONARY:
+ mUnpublishableNotInDictionary++;
+ break;
+ }
+ }
}
diff --git a/java/src/com/android/inputmethod/research/Uploader.java b/java/src/com/android/inputmethod/research/Uploader.java
index ba05ec12b..c7ea3e69d 100644
--- a/java/src/com/android/inputmethod/research/Uploader.java
+++ b/java/src/com/android/inputmethod/research/Uploader.java
@@ -49,7 +49,7 @@ public final class Uploader {
private static final boolean DEBUG = false
&& ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG;
// Set IS_INHIBITING_AUTO_UPLOAD to true for local testing
- private static final boolean IS_INHIBITING_AUTO_UPLOAD = false
+ private static final boolean IS_INHIBITING_UPLOAD = false
&& ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG;
private static final int BUF_SIZE = 1024 * 8;
@@ -76,7 +76,7 @@ public final class Uploader {
}
public boolean isPossibleToUpload() {
- return hasUploadingPermission() && mUrl != null && !IS_INHIBITING_AUTO_UPLOAD;
+ return hasUploadingPermission() && mUrl != null && !IS_INHIBITING_UPLOAD;
}
private boolean hasUploadingPermission() {
diff --git a/java/src/com/android/inputmethod/research/UploaderService.java b/java/src/com/android/inputmethod/research/UploaderService.java
index d2db34927..fd3f2f60e 100644
--- a/java/src/com/android/inputmethod/research/UploaderService.java
+++ b/java/src/com/android/inputmethod/research/UploaderService.java
@@ -24,8 +24,6 @@ import android.content.Intent;
import android.os.Bundle;
import android.os.SystemClock;
-import com.android.inputmethod.latin.define.ProductionFlag;
-
/**
* Service to invoke the uploader.
*
@@ -33,12 +31,9 @@ import com.android.inputmethod.latin.define.ProductionFlag;
*/
public final class UploaderService extends IntentService {
private static final String TAG = UploaderService.class.getSimpleName();
- private static final boolean DEBUG = false
- && ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG;
public static final long RUN_INTERVAL = AlarmManager.INTERVAL_HOUR;
public static final String EXTRA_UPLOAD_UNCONDITIONALLY = UploaderService.class.getName()
+ ".extra.UPLOAD_UNCONDITIONALLY";
- protected static final int TIMEOUT_IN_MS = 1000 * 4;
public UploaderService() {
super("Research Uploader Service");
diff --git a/java/src/com/android/inputmethod/research/ui/SplashScreen.java b/java/src/com/android/inputmethod/research/ui/SplashScreen.java
new file mode 100644
index 000000000..78ed668d1
--- /dev/null
+++ b/java/src/com/android/inputmethod/research/ui/SplashScreen.java
@@ -0,0 +1,111 @@
+/*
+ * 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.ui;
+
+import android.app.AlertDialog.Builder;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.Intent;
+import android.inputmethodservice.InputMethodService;
+import android.net.Uri;
+import android.os.IBinder;
+import android.view.Window;
+import android.view.WindowManager.LayoutParams;
+
+import com.android.inputmethod.latin.R.string;
+
+/**
+ * Show a dialog when the user first opens the keyboard.
+ *
+ * The splash screen is a modal dialog box presented when the user opens this keyboard for the first
+ * time. It is useful for giving specific warnings that must be shown to the user before use.
+ *
+ * While the splash screen does share with the setup wizard the common goal of presenting
+ * information to the user before use, they are presented at different times and with different
+ * capabilities. The setup wizard is launched by tapping on the icon, and walks the user through
+ * the setup process. It can, however, be bypassed by enabling the keyboard from Settings directly.
+ * The splash screen cannot be bypassed, and is therefore more appropriate for obtaining user
+ * consent.
+ */
+public class SplashScreen {
+ public interface UserConsentListener {
+ public void onSplashScreenUserClickedOk();
+ }
+
+ final UserConsentListener mListener;
+ final Dialog mSplashDialog;
+
+ public SplashScreen(final InputMethodService inputMethodService,
+ final UserConsentListener listener) {
+ mListener = listener;
+ final Builder builder = new Builder(inputMethodService)
+ .setTitle(string.research_splash_title)
+ .setMessage(string.research_splash_content)
+ .setPositiveButton(android.R.string.yes,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ mListener.onSplashScreenUserClickedOk();
+ mSplashDialog.dismiss();
+ }
+ })
+ .setNegativeButton(android.R.string.no,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ final String packageName = inputMethodService.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);
+ inputMethodService.startActivity(intent);
+ }
+ })
+ .setCancelable(true)
+ .setOnCancelListener(
+ new OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ inputMethodService.requestHideSelf(0);
+ }
+ });
+ mSplashDialog = builder.create();
+ }
+
+ /**
+ * Show the splash screen.
+ *
+ * The user must consent to the terms presented in the SplashScreen before they can use the
+ * keyboard. If they cancel instead, they are given the option to uninstall the keybard.
+ *
+ * @param windowToken {@link IBinder} to attach dialog to
+ */
+ public void showSplashScreen(final IBinder windowToken) {
+ final Window window = mSplashDialog.getWindow();
+ final LayoutParams lp = window.getAttributes();
+ lp.token = windowToken;
+ lp.type = LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
+ window.setAttributes(lp);
+ window.addFlags(LayoutParams.FLAG_ALT_FOCUSABLE_IM);
+ mSplashDialog.show();
+ }
+
+ public boolean isShowing() {
+ return mSplashDialog.isShowing();
+ }
+}