diff options
Diffstat (limited to 'java/src')
10 files changed, 466 insertions, 142 deletions
diff --git a/java/src/com/android/inputmethod/compat/CompatUtils.java b/java/src/com/android/inputmethod/compat/CompatUtils.java index a82103ab3..5a2b6bd2b 100644 --- a/java/src/com/android/inputmethod/compat/CompatUtils.java +++ b/java/src/com/android/inputmethod/compat/CompatUtils.java @@ -16,7 +16,6 @@ package com.android.inputmethod.compat; -import android.content.Intent; import android.text.TextUtils; import android.util.Log; @@ -26,23 +25,9 @@ import java.lang.reflect.Method; public final class CompatUtils { private static final String TAG = CompatUtils.class.getSimpleName(); - private static final String EXTRA_INPUT_METHOD_ID = "input_method_id"; - // TODO: Can these be constants instead of literal String constants? - private static final String INPUT_METHOD_SUBTYPE_SETTINGS = - "android.settings.INPUT_METHOD_SUBTYPE_SETTINGS"; - public static Intent getInputLanguageSelectionIntent(final String inputMethodId, - final int flagsForSubtypeSettings) { - // Refer to android.provider.Settings.ACTION_INPUT_METHOD_SUBTYPE_SETTINGS - final String action = INPUT_METHOD_SUBTYPE_SETTINGS; - final Intent intent = new Intent(action); - if (!TextUtils.isEmpty(inputMethodId)) { - intent.putExtra(EXTRA_INPUT_METHOD_ID, inputMethodId); - } - if (flagsForSubtypeSettings > 0) { - intent.setFlags(flagsForSubtypeSettings); - } - return intent; + private CompatUtils() { + // This utility class is not publicly instantiable. } public static Class<?> getClass(final String className) { diff --git a/java/src/com/android/inputmethod/latin/AdditionalSubtypeSettings.java b/java/src/com/android/inputmethod/latin/AdditionalSubtypeSettings.java index f7877226d..ff5e33949 100644 --- a/java/src/com/android/inputmethod/latin/AdditionalSubtypeSettings.java +++ b/java/src/com/android/inputmethod/latin/AdditionalSubtypeSettings.java @@ -44,8 +44,6 @@ import android.widget.Spinner; import android.widget.SpinnerAdapter; import android.widget.Toast; -import com.android.inputmethod.compat.CompatUtils; - import java.util.ArrayList; import java.util.TreeSet; @@ -519,7 +517,7 @@ public final class AdditionalSubtypeSettings extends PreferenceFragment { .setPositiveButton(R.string.enable, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - final Intent intent = CompatUtils.getInputLanguageSelectionIntent( + final Intent intent = IntentUtils.getInputLanguageSelectionIntent( mRichImm.getInputMethodIdOfThisIme(), Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED diff --git a/java/src/com/android/inputmethod/latin/IntentUtils.java b/java/src/com/android/inputmethod/latin/IntentUtils.java new file mode 100644 index 000000000..d175af504 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/IntentUtils.java @@ -0,0 +1,45 @@ +/* + * 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.latin; + +import android.content.Intent; +import android.text.TextUtils; + +public final class IntentUtils { + private static final String EXTRA_INPUT_METHOD_ID = "input_method_id"; + // TODO: Can these be constants instead of literal String constants? + private static final String INPUT_METHOD_SUBTYPE_SETTINGS = + "android.settings.INPUT_METHOD_SUBTYPE_SETTINGS"; + + private IntentUtils() { + // This utility class is not publicly instantiable. + } + + public static Intent getInputLanguageSelectionIntent(final String inputMethodId, + final int flagsForSubtypeSettings) { + // Refer to android.provider.Settings.ACTION_INPUT_METHOD_SUBTYPE_SETTINGS + final String action = INPUT_METHOD_SUBTYPE_SETTINGS; + final Intent intent = new Intent(action); + if (!TextUtils.isEmpty(inputMethodId)) { + intent.putExtra(EXTRA_INPUT_METHOD_ID, inputMethodId); + } + if (flagsForSubtypeSettings > 0) { + intent.setFlags(flagsForSubtypeSettings); + } + return intent; + } +} diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index 08217326a..1c49bb0cc 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -62,7 +62,6 @@ import android.view.inputmethod.InputMethodSubtype; import com.android.inputmethod.accessibility.AccessibilityUtils; import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy; import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.compat.CompatUtils; import com.android.inputmethod.compat.InputMethodServiceCompatUtils; import com.android.inputmethod.compat.SuggestionSpanUtils; import com.android.inputmethod.event.EventInterpreter; @@ -2512,7 +2511,7 @@ public final class LatinIME extends InputMethodService implements KeyboardAction di.dismiss(); switch (position) { case 0: - Intent intent = CompatUtils.getInputLanguageSelectionIntent( + final Intent intent = IntentUtils.getInputLanguageSelectionIntent( mRichImm.getInputMethodIdOfThisIme(), Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED diff --git a/java/src/com/android/inputmethod/research/FeedbackFragment.java b/java/src/com/android/inputmethod/research/FeedbackFragment.java index 69ddf82ea..39f9c87a0 100644 --- a/java/src/com/android/inputmethod/research/FeedbackFragment.java +++ b/java/src/com/android/inputmethod/research/FeedbackFragment.java @@ -36,8 +36,8 @@ import com.android.inputmethod.latin.R; public class FeedbackFragment extends Fragment implements OnClickListener { private static final String TAG = FeedbackFragment.class.getSimpleName(); - private static final String KEY_FEEDBACK_STRING = "FeedbackString"; - private static final String KEY_INCLUDE_ACCOUNT_NAME = "IncludeAccountName"; + public static final String KEY_FEEDBACK_STRING = "FeedbackString"; + public static final String KEY_INCLUDE_ACCOUNT_NAME = "IncludeAccountName"; public static final String KEY_HAS_USER_RECORDING = "HasRecording"; private EditText mEditText; diff --git a/java/src/com/android/inputmethod/research/LogStatement.java b/java/src/com/android/inputmethod/research/LogStatement.java index 1d83e1a86..059146ae6 100644 --- a/java/src/com/android/inputmethod/research/LogStatement.java +++ b/java/src/com/android/inputmethod/research/LogStatement.java @@ -16,6 +16,18 @@ package com.android.inputmethod.research; +import android.content.SharedPreferences; +import android.util.JsonWriter; +import android.util.Log; +import android.view.MotionEvent; +import android.view.inputmethod.CompletionInfo; + +import com.android.inputmethod.keyboard.Key; +import com.android.inputmethod.latin.SuggestedWords; +import com.android.inputmethod.latin.define.ProductionFlag; + +import java.io.IOException; + /** * A template for typed information stored in the logs. * @@ -24,6 +36,9 @@ package com.android.inputmethod.research; * actual values are stored separately. */ class LogStatement { + private static final String TAG = LogStatement.class.getSimpleName(); + private static final boolean DEBUG = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG; + // Constants for particular statements public static final String TYPE_POINTER_TRACKER_CALL_LISTENER_ON_CODE_INPUT = "PointerTrackerCallListenerOnCodeInput"; @@ -36,6 +51,11 @@ class LogStatement { public static final String TYPE_MOTION_EVENT = "MotionEvent"; public static final String KEY_IS_LOGGING_RELATED = "isLoggingRelated"; + // Keys for internal key/value pairs + private static final String CURRENT_TIME_KEY = "_ct"; + private static final String UPTIME_KEY = "_ut"; + private static final String EVENT_TYPE_KEY = "_ty"; + // Name specifying the LogStatement type. private final String mType; @@ -142,4 +162,61 @@ class LogStatement { } return false; } + + /** + * Write the contents out through jsonWriter. + * + * Note that this method is not thread safe for the same jsonWriter. Callers must ensure + * thread safety. + */ + public boolean outputToLocked(final JsonWriter jsonWriter, final Long time, + final Object... values) { + if (DEBUG) { + if (mKeys.length != values.length) { + Log.d(TAG, "Key and Value list sizes do not match. " + mType); + } + } + try { + jsonWriter.beginObject(); + jsonWriter.name(CURRENT_TIME_KEY).value(System.currentTimeMillis()); + jsonWriter.name(UPTIME_KEY).value(time); + jsonWriter.name(EVENT_TYPE_KEY).value(mType); + final int length = values.length; + for (int i = 0; i < length; i++) { + jsonWriter.name(mKeys[i]); + final Object value = values[i]; + if (value instanceof CharSequence) { + jsonWriter.value(value.toString()); + } else if (value instanceof Number) { + jsonWriter.value((Number) value); + } else if (value instanceof Boolean) { + jsonWriter.value((Boolean) value); + } else if (value instanceof CompletionInfo[]) { + JsonUtils.writeJson((CompletionInfo[]) value, jsonWriter); + } else if (value instanceof SharedPreferences) { + JsonUtils.writeJson((SharedPreferences) value, jsonWriter); + } else if (value instanceof Key[]) { + JsonUtils.writeJson((Key[]) value, jsonWriter); + } else if (value instanceof SuggestedWords) { + JsonUtils.writeJson((SuggestedWords) value, jsonWriter); + } else if (value instanceof MotionEvent) { + JsonUtils.writeJson((MotionEvent) value, jsonWriter); + } else if (value == null) { + jsonWriter.nullValue(); + } else { + if (DEBUG) { + Log.w(TAG, "Unrecognized type to be logged: " + + (value == null ? "<null>" : value.getClass().getName())); + } + jsonWriter.nullValue(); + } + } + jsonWriter.endObject(); + } catch (IOException e) { + e.printStackTrace(); + Log.w(TAG, "Error in JsonWriter; skipping LogStatement"); + return false; + } + return true; + } } diff --git a/java/src/com/android/inputmethod/research/LogUnit.java b/java/src/com/android/inputmethod/research/LogUnit.java index 2e732fc6c..a584a3af6 100644 --- a/java/src/com/android/inputmethod/research/LogUnit.java +++ b/java/src/com/android/inputmethod/research/LogUnit.java @@ -17,13 +17,11 @@ package com.android.inputmethod.research; import android.content.SharedPreferences; +import android.os.SystemClock; import android.text.TextUtils; import android.util.JsonWriter; import android.util.Log; -import android.view.MotionEvent; -import android.view.inputmethod.CompletionInfo; -import com.android.inputmethod.keyboard.Key; import com.android.inputmethod.latin.SuggestedWords; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.define.ProductionFlag; @@ -153,11 +151,10 @@ import java.util.List; jsonWriter = researchLog.getValidJsonWriterLocked(); outputLogUnitStart(jsonWriter, canIncludePrivateData); } - outputLogStatementToLocked(jsonWriter, mLogStatementList.get(i), mValuesList.get(i), - mTimeList.get(i)); + logStatement.outputToLocked(jsonWriter, mTimeList.get(i), mValuesList.get(i)); if (DEBUG) { - outputLogStatementToLocked(debugJsonWriter, mLogStatementList.get(i), - mValuesList.get(i), mTimeList.get(i)); + logStatement.outputToLocked(debugJsonWriter, mTimeList.get(i), + mValuesList.get(i)); } } if (jsonWriter != null) { @@ -180,97 +177,34 @@ import java.util.List; } } - private static final String CURRENT_TIME_KEY = "_ct"; - private static final String UPTIME_KEY = "_ut"; - private static final String EVENT_TYPE_KEY = "_ty"; private static final String WORD_KEY = "_wo"; private static final String CORRECTION_TYPE_KEY = "_corType"; private static final String LOG_UNIT_BEGIN_KEY = "logUnitStart"; private static final String LOG_UNIT_END_KEY = "logUnitEnd"; + final LogStatement LOGSTATEMENT_LOG_UNIT_BEGIN_WITH_PRIVATE_DATA = + new LogStatement(LOG_UNIT_BEGIN_KEY, false /* isPotentiallyPrivate */, + false /* isPotentiallyRevealing */, WORD_KEY, CORRECTION_TYPE_KEY); + final LogStatement LOGSTATEMENT_LOG_UNIT_BEGIN_WITHOUT_PRIVATE_DATA = + new LogStatement(LOG_UNIT_BEGIN_KEY, false /* isPotentiallyPrivate */, + false /* isPotentiallyRevealing */); private void outputLogUnitStart(final JsonWriter jsonWriter, final boolean canIncludePrivateData) { - try { - jsonWriter.beginObject(); - jsonWriter.name(CURRENT_TIME_KEY).value(System.currentTimeMillis()); - if (canIncludePrivateData) { - jsonWriter.name(WORD_KEY).value(getWord()); - jsonWriter.name(CORRECTION_TYPE_KEY).value(getCorrectionType()); - } - jsonWriter.name(EVENT_TYPE_KEY).value(LOG_UNIT_BEGIN_KEY); - jsonWriter.endObject(); - } catch (IOException e) { - e.printStackTrace(); - Log.w(TAG, "Error in JsonWriter; cannot write LogUnitStart"); + final LogStatement logStatement; + if (canIncludePrivateData) { + LOGSTATEMENT_LOG_UNIT_BEGIN_WITH_PRIVATE_DATA.outputToLocked(jsonWriter, + SystemClock.uptimeMillis(), getWord(), getCorrectionType()); + } else { + LOGSTATEMENT_LOG_UNIT_BEGIN_WITHOUT_PRIVATE_DATA.outputToLocked(jsonWriter, + SystemClock.uptimeMillis()); } } + final LogStatement LOGSTATEMENT_LOG_UNIT_END = + new LogStatement(LOG_UNIT_END_KEY, false /* isPotentiallyPrivate */, + false /* isPotentiallyRevealing */); private void outputLogUnitStop(final JsonWriter jsonWriter) { - try { - jsonWriter.beginObject(); - jsonWriter.name(CURRENT_TIME_KEY).value(System.currentTimeMillis()); - jsonWriter.name(EVENT_TYPE_KEY).value(LOG_UNIT_END_KEY); - jsonWriter.endObject(); - } catch (IOException e) { - e.printStackTrace(); - Log.w(TAG, "Error in JsonWriter; cannot write LogUnitStop"); - } - } - - /** - * 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.getKeys().length != values.length) { - Log.d(TAG, "Key and Value list sizes do not match. " + logStatement.getType()); - } - } - try { - jsonWriter.beginObject(); - jsonWriter.name(CURRENT_TIME_KEY).value(System.currentTimeMillis()); - jsonWriter.name(UPTIME_KEY).value(time); - jsonWriter.name(EVENT_TYPE_KEY).value(logStatement.getType()); - final String[] keys = logStatement.getKeys(); - 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 instanceof MotionEvent) { - JsonUtils.writeJson((MotionEvent) 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; + LOGSTATEMENT_LOG_UNIT_END.outputToLocked(jsonWriter, SystemClock.uptimeMillis()); } /** diff --git a/java/src/com/android/inputmethod/research/MotionEventReader.java b/java/src/com/android/inputmethod/research/MotionEventReader.java index 26a1d7f55..e59adfa19 100644 --- a/java/src/com/android/inputmethod/research/MotionEventReader.java +++ b/java/src/com/android/inputmethod/research/MotionEventReader.java @@ -19,6 +19,8 @@ package com.android.inputmethod.research; import android.util.JsonReader; import android.util.Log; import android.view.MotionEvent; +import android.view.MotionEvent.PointerCoords; +import android.view.MotionEvent.PointerProperties; import com.android.inputmethod.latin.define.ProductionFlag; @@ -33,6 +35,14 @@ import java.util.ArrayList; public class MotionEventReader { private static final String TAG = MotionEventReader.class.getSimpleName(); private static final boolean DEBUG = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG; + // Assumes that MotionEvent.ACTION_MASK does not have all bits set.` + private static final int UNINITIALIZED_ACTION = ~MotionEvent.ACTION_MASK; + // No legitimate int is negative + private static final int UNINITIALIZED_INT = -1; + // No legitimate long is negative + private static final long UNINITIALIZED_LONG = -1L; + // No legitimate float is negative + private static final float UNINITIALIZED_FLOAT = -1.0f; public ReplayData readMotionEventData(final File file) { final ReplayData replayData = new ReplayData(); @@ -55,19 +65,82 @@ public class MotionEventReader { static class ReplayData { final ArrayList<Integer> mActions = new ArrayList<Integer>(); - final ArrayList<Integer> mXCoords = new ArrayList<Integer>(); - final ArrayList<Integer> mYCoords = new ArrayList<Integer>(); + final ArrayList<PointerProperties[]> mPointerPropertiesArrays + = new ArrayList<PointerProperties[]>(); + final ArrayList<PointerCoords[]> mPointerCoordsArrays = new ArrayList<PointerCoords[]>(); final ArrayList<Long> mTimes = new ArrayList<Long>(); } - private void readLogStatement(final JsonReader jsonReader, final ReplayData replayData) - throws IOException { + /** + * Read motion data from a logStatement and store it in {@code replayData}. + * + * Two kinds of logStatements can be read. In the first variant, the MotionEvent data is + * represented as attributes at the top level like so: + * + * <pre> + * { + * "_ct": 1359590400000, + * "_ut": 4381933, + * "_ty": "MotionEvent", + * "action": "UP", + * "isLoggingRelated": false, + * "x": 100, + * "y": 200 + * } + * </pre> + * + * In the second variant, there is a separate attribute for the MotionEvent that includes + * historical data if present: + * + * <pre> + * { + * "_ct": 135959040000, + * "_ut": 4382702, + * "_ty": "MotionEvent", + * "action": "MOVE", + * "isLoggingRelated": false, + * "motionEvent": { + * "pointerIds": [ + * 0 + * ], + * "xyt": [ + * { + * "t": 4382551, + * "d": [ + * { + * "x": 141.25, + * "y": 151.8485107421875, + * "toma": 101.82337188720703, + * "tomi": 101.82337188720703, + * "o": 0.0 + * } + * ] + * }, + * { + * "t": 4382559, + * "d": [ + * { + * "x": 140.7266082763672, + * "y": 151.8485107421875, + * "toma": 101.82337188720703, + * "tomi": 101.82337188720703, + * "o": 0.0 + * } + * ] + * } + * ] + * } + * }, + * </pre> + */ + /* package for test */ void readLogStatement(final JsonReader jsonReader, + final ReplayData replayData) throws IOException { String logStatementType = null; - Integer actionType = null; - Integer x = null; - Integer y = null; - Long time = null; - boolean loggingRelated = false; + int actionType = UNINITIALIZED_ACTION; + int x = UNINITIALIZED_INT; + int y = UNINITIALIZED_INT; + long time = UNINITIALIZED_LONG; + boolean isLoggingRelated = false; jsonReader.beginObject(); while (jsonReader.hasNext()) { @@ -90,7 +163,18 @@ public class MotionEventReader { actionType = MotionEvent.ACTION_MOVE; } } else if (key.equals("loggingRelated")) { - loggingRelated = jsonReader.nextBoolean(); + isLoggingRelated = jsonReader.nextBoolean(); + } else if (logStatementType != null && logStatementType.equals("MotionEvent") + && key.equals("motionEvent")) { + if (actionType == UNINITIALIZED_ACTION) { + Log.e(TAG, "no actionType assigned in MotionEvent json"); + } + // Second variant of LogStatement. + if (isLoggingRelated) { + jsonReader.skipValue(); + } else { + readEmbeddedMotionEvent(jsonReader, replayData, actionType); + } } else { if (DEBUG) { Log.w(TAG, "Unknown JSON key in LogStatement: " + key); @@ -100,14 +184,149 @@ public class MotionEventReader { } jsonReader.endObject(); - if (logStatementType != null && time != null && x != null && y != null && actionType != null - && logStatementType.equals("MotionEvent") - && !loggingRelated) { - replayData.mActions.add(actionType); - replayData.mXCoords.add(x); - replayData.mYCoords.add(y); - replayData.mTimes.add(time); + if (logStatementType != null && time != UNINITIALIZED_LONG && x != UNINITIALIZED_INT + && y != UNINITIALIZED_INT && actionType != UNINITIALIZED_ACTION + && logStatementType.equals("MotionEvent") && !isLoggingRelated) { + // First variant of LogStatement. + final PointerProperties pointerProperties = new PointerProperties(); + pointerProperties.id = 0; + pointerProperties.toolType = MotionEvent.TOOL_TYPE_UNKNOWN; + final PointerProperties[] pointerPropertiesArray = { + pointerProperties + }; + final PointerCoords pointerCoords = new PointerCoords(); + pointerCoords.x = x; + pointerCoords.y = y; + pointerCoords.pressure = 1.0f; + pointerCoords.size = 1.0f; + final PointerCoords[] pointerCoordsArray = { + pointerCoords + }; + addMotionEventData(replayData, actionType, time, pointerPropertiesArray, + pointerCoordsArray); + } + } + + private void readEmbeddedMotionEvent(final JsonReader jsonReader, final ReplayData replayData, + final int actionType) throws IOException { + jsonReader.beginObject(); + PointerProperties[] pointerPropertiesArray = null; + while (jsonReader.hasNext()) { // pointerIds/xyt + final String name = jsonReader.nextName(); + if (name.equals("pointerIds")) { + pointerPropertiesArray = readPointerProperties(jsonReader); + } else if (name.equals("xyt")) { + readPointerData(jsonReader, replayData, actionType, pointerPropertiesArray); + } + } + jsonReader.endObject(); + } + + private PointerProperties[] readPointerProperties(final JsonReader jsonReader) + throws IOException { + final ArrayList<PointerProperties> pointerPropertiesArrayList = + new ArrayList<PointerProperties>(); + jsonReader.beginArray(); + while (jsonReader.hasNext()) { + final PointerProperties pointerProperties = new PointerProperties(); + pointerProperties.id = jsonReader.nextInt(); + pointerProperties.toolType = MotionEvent.TOOL_TYPE_UNKNOWN; + pointerPropertiesArrayList.add(pointerProperties); + } + jsonReader.endArray(); + return pointerPropertiesArrayList.toArray( + new PointerProperties[pointerPropertiesArrayList.size()]); + } + + private void readPointerData(final JsonReader jsonReader, final ReplayData replayData, + final int actionType, final PointerProperties[] pointerPropertiesArray) + throws IOException { + if (pointerPropertiesArray == null) { + Log.e(TAG, "PointerIDs must be given before xyt data in json for MotionEvent"); + jsonReader.skipValue(); + return; + } + long time = UNINITIALIZED_LONG; + jsonReader.beginArray(); + while (jsonReader.hasNext()) { // Array of historical data + jsonReader.beginObject(); + final ArrayList<PointerCoords> pointerCoordsArrayList = new ArrayList<PointerCoords>(); + while (jsonReader.hasNext()) { // Time/data object + final String name = jsonReader.nextName(); + if (name.equals("t")) { + time = jsonReader.nextLong(); + } else if (name.equals("d")) { + jsonReader.beginArray(); + while (jsonReader.hasNext()) { // array of data per pointer + final PointerCoords pointerCoords = readPointerCoords(jsonReader); + if (pointerCoords != null) { + pointerCoordsArrayList.add(pointerCoords); + } + } + jsonReader.endArray(); + } else { + jsonReader.skipValue(); + } + } + jsonReader.endObject(); + // Data was recorded as historical events, but must be split apart into + // separate MotionEvents for replaying + if (time != UNINITIALIZED_LONG) { + addMotionEventData(replayData, actionType, time, pointerPropertiesArray, + pointerCoordsArrayList.toArray( + new PointerCoords[pointerCoordsArrayList.size()])); + } else { + Log.e(TAG, "Time not assigned in json for MotionEvent"); + } + } + jsonReader.endArray(); + } + + private PointerCoords readPointerCoords(final JsonReader jsonReader) throws IOException { + jsonReader.beginObject(); + float x = UNINITIALIZED_FLOAT; + float y = UNINITIALIZED_FLOAT; + while (jsonReader.hasNext()) { // x,y + final String name = jsonReader.nextName(); + if (name.equals("x")) { + x = (float) jsonReader.nextDouble(); + } else if (name.equals("y")) { + y = (float) jsonReader.nextDouble(); + } else { + jsonReader.skipValue(); + } + } + jsonReader.endObject(); + + if (Float.compare(x, UNINITIALIZED_FLOAT) == 0 + || Float.compare(y, UNINITIALIZED_FLOAT) == 0) { + Log.w(TAG, "missing x or y value in MotionEvent json"); + return null; } + final PointerCoords pointerCoords = new PointerCoords(); + pointerCoords.x = x; + pointerCoords.y = y; + pointerCoords.pressure = 1.0f; + pointerCoords.size = 1.0f; + return pointerCoords; + } + + /** + * Tests that {@code x} is uninitialized. + * + * Assumes that {@code x} will never be given a valid value less than 0, and that + * UNINITIALIZED_FLOAT is less than 0.0f. + */ + private boolean isUninitializedFloat(final float x) { + return x < 0.0f; } + private void addMotionEventData(final ReplayData replayData, final int actionType, + final long time, final PointerProperties[] pointerProperties, + final PointerCoords[] pointerCoords) { + replayData.mActions.add(actionType); + replayData.mTimes.add(time); + replayData.mPointerPropertiesArrays.add(pointerProperties); + replayData.mPointerCoordsArrays.add(pointerCoords); + } } diff --git a/java/src/com/android/inputmethod/research/Replayer.java b/java/src/com/android/inputmethod/research/Replayer.java index 611abb288..a9b7a9d0c 100644 --- a/java/src/com/android/inputmethod/research/Replayer.java +++ b/java/src/com/android/inputmethod/research/Replayer.java @@ -22,6 +22,8 @@ import android.os.Message; import android.os.SystemClock; import android.util.Log; import android.view.MotionEvent; +import android.view.MotionEvent.PointerCoords; +import android.view.MotionEvent.PointerProperties; import com.android.inputmethod.keyboard.KeyboardSwitcher; import com.android.inputmethod.keyboard.MainKeyboardView; @@ -62,7 +64,6 @@ public class Replayer { if (mIsReplaying) { return; } - mIsReplaying = true; final int numActions = replayData.mActions.size(); if (DEBUG) { @@ -95,25 +96,36 @@ public class Replayer { case MSG_MOTION_EVENT: final int index = msg.arg1; final int action = replayData.mActions.get(index); - final int x = replayData.mXCoords.get(index); - final int y = replayData.mYCoords.get(index); + final PointerProperties[] pointerPropertiesArray = + replayData.mPointerPropertiesArrays.get(index); + final PointerCoords[] pointerCoordsArray = + replayData.mPointerCoordsArrays.get(index); final long origTime = replayData.mTimes.get(index); if (action == MotionEvent.ACTION_DOWN) { mOrigDownTime = origTime; } final MotionEvent me = MotionEvent.obtain(mOrigDownTime + timeAdjustment, - origTime + timeAdjustment, action, x, y, 0); + origTime + timeAdjustment, action, + pointerPropertiesArray.length, pointerPropertiesArray, + pointerCoordsArray, 0, 0, 1.0f, 1.0f, 0, 0, 0, 0); mainKeyboardView.processMotionEvent(me); me.recycle(); break; case MSG_DONE: mIsReplaying = false; + ResearchLogger.getInstance().requestIndicatorRedraw(); break; } } }; + handler.post(new Runnable() { + @Override + public void run() { + ResearchLogger.getInstance().requestIndicatorRedraw(); + } + }); for (int i = 0; i < numActions; i++) { final Message msg = Message.obtain(handler, MSG_MOTION_EVENT, i, 0); final long msgTime = replayData.mTimes.get(i) + timeAdjustment; @@ -130,4 +142,8 @@ public class Replayer { handler.postAtTime(callback, presentDoneTime + 1); } } + + public boolean isReplaying() { + return mIsReplaying; + } } diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java index da410010f..364ab2da2 100644 --- a/java/src/com/android/inputmethod/research/ResearchLogger.java +++ b/java/src/com/android/inputmethod/research/ResearchLogger.java @@ -115,6 +115,12 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang // private info. private static final boolean LOG_FULL_TEXTVIEW_CONTENTS = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG; + // Whether the feedback dialog preserves the editable text across invocations. Should be false + // for normal research builds so users do not have to delete the same feedback string they + // entered earlier. Should be true for builds internal to a development team so when the text + // field holds a channel name, the developer does not have to re-enter it when using the + // feedback mechanism to generate multiple tests. + private static final boolean FEEDBACK_DIALOG_SHOULD_PRESERVE_TEXT_FIELD = false; public static final boolean DEFAULT_USABILITY_STUDY_MODE = false; /* package */ static boolean sIsLogging = false; private static final int OUTPUT_FORMAT_VERSION = 5; @@ -140,6 +146,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang private static final String WHITESPACE_SEPARATORS = " \t\n\r"; private static final int MAX_INPUTVIEW_LENGTH_TO_CAPTURE = 8192; // must be >=1 private static final String PREF_RESEARCH_LOGGER_UUID_STRING = "pref_research_logger_uuid"; + private static final String PREF_RESEARCH_SAVED_CHANNEL = "pref_research_saved_channel"; private static final ResearchLogger sInstance = new ResearchLogger(); private static String sAccountType = null; @@ -591,12 +598,20 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang mFeedbackLogBuffer = null; mFeedbackLog = null; - Intent intent = new Intent(); + final Intent intent = new Intent(); intent.setClass(mLatinIME, FeedbackActivity.class); - if (mFeedbackDialogBundle != null) { - Log.d(TAG, "putting extra in feedbackdialogbundle"); - intent.putExtras(mFeedbackDialogBundle); + if (mFeedbackDialogBundle == null) { + // Restore feedback field with channel name + final Bundle bundle = new Bundle(); + bundle.putBoolean(FeedbackFragment.KEY_INCLUDE_ACCOUNT_NAME, true); + bundle.putBoolean(FeedbackFragment.KEY_HAS_USER_RECORDING, false); + if (FEEDBACK_DIALOG_SHOULD_PRESERVE_TEXT_FIELD) { + final String savedChannelName = mPrefs.getString(PREF_RESEARCH_SAVED_CHANNEL, ""); + bundle.putString(FeedbackFragment.KEY_FEEDBACK_STRING, savedChannelName); + } + mFeedbackDialogBundle = bundle; } + intent.putExtras(mFeedbackDialogBundle); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); latinIME.startActivity(intent); } @@ -787,6 +802,18 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } }, 1000); } + + if (FEEDBACK_DIALOG_SHOULD_PRESERVE_TEXT_FIELD) { + // Use feedback string as a channel name to label feedback strings. Here we record the + // string for prepopulating the field next time. + final String channelName = feedbackContents; + if (mPrefs == null) { + return; + } + final Editor e = mPrefs.edit(); + e.putString(PREF_RESEARCH_SAVED_CHANNEL, channelName); + e.apply(); + } } public void uploadNow() { @@ -819,7 +846,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } private boolean isAllowedToLog() { - return !mIsPasswordView && !mIsLoggingSuspended && sIsLogging && !mInFeedbackDialog; + return !mIsPasswordView && !mIsLoggingSuspended && sIsLogging && !mInFeedbackDialog + && !isReplaying(); } public void requestIndicatorRedraw() { @@ -832,15 +860,30 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang mMainKeyboardView.invalidateAllKeys(); } + private boolean isReplaying() { + return mReplayer.isReplaying(); + } + + private int getIndicatorColor() { + if (isMakingUserRecording()) { + return Color.YELLOW; + } + if (isReplaying()) { + return Color.GREEN; + } + return Color.RED; + } + public void paintIndicator(KeyboardView view, Paint paint, Canvas canvas, int width, int height) { // TODO: Reimplement using a keyboard background image specific to the ResearchLogger // and remove this method. // The check for MainKeyboardView ensures that the indicator only decorates the main // keyboard, not every keyboard. - if (IS_SHOWING_INDICATOR && isAllowedToLog() && view instanceof MainKeyboardView) { + if (IS_SHOWING_INDICATOR && (isAllowedToLog() || isReplaying()) + && view instanceof MainKeyboardView) { final int savedColor = paint.getColor(); - paint.setColor(isMakingUserRecording() ? Color.YELLOW : Color.RED); + paint.setColor(getIndicatorColor()); final Style savedStyle = paint.getStyle(); paint.setStyle(Style.STROKE); final float savedStrokeWidth = paint.getStrokeWidth(); @@ -960,15 +1003,23 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } } + /** + * Publish all the logUnits in the logBuffer, without doing any privacy filtering. + */ /* package for test */ void publishLogBuffer(final LogBuffer logBuffer, - final ResearchLog researchLog, final boolean isIncludingPrivateData) { - publishLogUnits(logBuffer.getLogUnits(), researchLog, isIncludingPrivateData); + final ResearchLog researchLog, final boolean canIncludePrivateData) { + publishLogUnits(logBuffer.getLogUnits(), researchLog, canIncludePrivateData); } private static final LogStatement LOGSTATEMENT_LOG_SEGMENT_OPENING = new LogStatement("logSegmentStart", false, false, "isIncludingPrivateData"); private static final LogStatement LOGSTATEMENT_LOG_SEGMENT_CLOSING = new LogStatement("logSegmentEnd", false, false); + /** + * Publish all LogUnits in a list. + * + * Any privacy checks should be performed before calling this method. + */ /* package for test */ void publishLogUnits(final List<LogUnit> logUnits, final ResearchLog researchLog, final boolean canIncludePrivateData) { final LogUnit openingLogUnit = new LogUnit(); @@ -1349,7 +1400,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang final int index, final String suggestion, final boolean isBatchMode) { final ResearchLogger researchLogger = getInstance(); if (!replacedWord.equals(suggestion.toString())) { - // The user choose something other than what was already there. + // The user chose something other than what was already there. researchLogger.setCurrentLogUnitContainsCorrection(); researchLogger.setCurrentLogUnitCorrectionType(LogUnit.CORRECTIONTYPE_TYPO); } |