aboutsummaryrefslogtreecommitdiffstats
path: root/java/src
diff options
context:
space:
mode:
Diffstat (limited to 'java/src')
-rw-r--r--java/src/com/android/inputmethod/keyboard/Key.java20
-rw-r--r--java/src/com/android/inputmethod/keyboard/Keyboard.java10
-rw-r--r--java/src/com/android/inputmethod/keyboard/KeyboardId.java14
-rw-r--r--java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java41
-rw-r--r--java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java4
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java84
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeyboardParams.java4
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeyboardRow.java26
-rw-r--r--java/src/com/android/inputmethod/latin/AssetFileAddress.java9
-rw-r--r--java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java2
-rw-r--r--java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java169
-rw-r--r--java/src/com/android/inputmethod/latin/DictionaryFactory.java51
-rw-r--r--java/src/com/android/inputmethod/latin/DictionaryInfoUtils.java245
-rw-r--r--java/src/com/android/inputmethod/latin/ExternalDictionaryGetterForDebug.java20
-rw-r--r--java/src/com/android/inputmethod/latin/LatinIME.java2
-rw-r--r--java/src/com/android/inputmethod/research/FeedbackActivity.java15
-rw-r--r--java/src/com/android/inputmethod/research/FeedbackFragment.java126
-rw-r--r--java/src/com/android/inputmethod/research/LogStatement.java146
-rw-r--r--java/src/com/android/inputmethod/research/LogUnit.java139
-rw-r--r--java/src/com/android/inputmethod/research/MotionEventReader.java113
-rw-r--r--java/src/com/android/inputmethod/research/Replayer.java120
-rw-r--r--java/src/com/android/inputmethod/research/ResearchLogger.java321
-rw-r--r--java/src/com/android/inputmethod/research/UploaderService.java4
23 files changed, 1207 insertions, 478 deletions
diff --git a/java/src/com/android/inputmethod/keyboard/Key.java b/java/src/com/android/inputmethod/keyboard/Key.java
index 9b971755e..1e5af5154 100644
--- a/java/src/com/android/inputmethod/keyboard/Key.java
+++ b/java/src/com/android/inputmethod/keyboard/Key.java
@@ -41,7 +41,6 @@ import com.android.inputmethod.keyboard.internal.KeyboardRow;
import com.android.inputmethod.keyboard.internal.MoreKeySpec;
import com.android.inputmethod.latin.Constants;
import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.ResourceUtils;
import com.android.inputmethod.latin.StringUtils;
import org.xmlpull.v1.XmlPullParser;
@@ -225,8 +224,8 @@ public class Key implements Comparable<Key> {
public Key(final Resources res, final KeyboardParams params, final KeyboardRow row,
final XmlPullParser parser) throws XmlPullParserException {
final float horizontalGap = isSpacer() ? 0 : params.mHorizontalGap;
- final int keyHeight = row.mRowHeight;
- mHeight = keyHeight - params.mVerticalGap;
+ final int rowHeight = row.mRowHeight;
+ mHeight = rowHeight - params.mVerticalGap;
final TypedArray keyAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
R.styleable.Keyboard_Key);
@@ -241,17 +240,18 @@ public class Key implements Comparable<Key> {
mY = keyYPos;
mWidth = Math.round(keyWidth - horizontalGap);
mHitBox.set(Math.round(keyXPos), keyYPos, Math.round(keyXPos + keyWidth) + 1,
- keyYPos + keyHeight);
+ keyYPos + rowHeight);
// Update row to have current x coordinate.
row.setXPos(keyXPos + keyWidth);
mBackgroundType = style.getInt(keyAttr,
R.styleable.Keyboard_Key_backgroundType, row.getDefaultBackgroundType());
- final int visualInsetsLeft = Math.round(ResourceUtils.getDimensionOrFraction(keyAttr,
- R.styleable.Keyboard_Key_visualInsetsLeft, params.mBaseWidth, 0));
- final int visualInsetsRight = Math.round(ResourceUtils.getDimensionOrFraction(keyAttr,
- R.styleable.Keyboard_Key_visualInsetsRight, params.mBaseWidth, 0));
+ final int baseWidth = params.mBaseWidth;
+ final int visualInsetsLeft = Math.round(keyAttr.getFraction(
+ R.styleable.Keyboard_Key_visualInsetsLeft, baseWidth, baseWidth, 0));
+ final int visualInsetsRight = Math.round(keyAttr.getFraction(
+ R.styleable.Keyboard_Key_visualInsetsRight, baseWidth, baseWidth, 0));
mIconId = KeySpecParser.getIconId(style.getString(keyAttr,
R.styleable.Keyboard_Key_keyIcon));
final int disabledIconId = KeySpecParser.getIconId(style.getString(keyAttr,
@@ -470,11 +470,11 @@ public class Key implements Comparable<Key> {
}
public void markAsLeftEdge(final KeyboardParams params) {
- mHitBox.left = params.mHorizontalEdgesPadding;
+ mHitBox.left = params.mLeftPadding;
}
public void markAsRightEdge(final KeyboardParams params) {
- mHitBox.right = params.mOccupiedWidth - params.mHorizontalEdgesPadding;
+ mHitBox.right = params.mOccupiedWidth - params.mRightPadding;
}
public void markAsTopEdge(final KeyboardParams params) {
diff --git a/java/src/com/android/inputmethod/keyboard/Keyboard.java b/java/src/com/android/inputmethod/keyboard/Keyboard.java
index c2036fc43..e87ecbc7e 100644
--- a/java/src/com/android/inputmethod/keyboard/Keyboard.java
+++ b/java/src/com/android/inputmethod/keyboard/Keyboard.java
@@ -30,11 +30,11 @@ import com.android.inputmethod.latin.Constants;
* <p>The layout file for a keyboard contains XML that looks like the following snippet:</p>
* <pre>
* &lt;Keyboard
- * latin:keyWidth="%10p"
- * latin:keyHeight="50px"
- * latin:horizontalGap="2px"
- * latin:verticalGap="2px" &gt;
- * &lt;Row latin:keyWidth="32px" &gt;
+ * latin:keyWidth="10%p"
+ * latin:rowHeight="50px"
+ * latin:horizontalGap="2%p"
+ * latin:verticalGap="2%p" &gt;
+ * &lt;Row latin:keyWidth="10%p" &gt;
* &lt;Key latin:keyLabel="A" /&gt;
* ...
* &lt;/Row&gt;
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardId.java b/java/src/com/android/inputmethod/keyboard/KeyboardId.java
index 4b43bcc5c..ee8ee9a4f 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardId.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardId.java
@@ -18,6 +18,7 @@ package com.android.inputmethod.keyboard;
import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET;
+import android.content.res.Configuration;
import android.text.InputType;
import android.text.TextUtils;
import android.view.inputmethod.EditorInfo;
@@ -62,8 +63,10 @@ public final class KeyboardId {
public final InputMethodSubtype mSubtype;
public final Locale mLocale;
public final int mDeviceFormFactor;
+ // TODO: Remove this member. It is used only for logging purpose.
public final int mOrientation;
public final int mWidth;
+ public final int mHeight;
public final int mMode;
public final int mElementId;
private final EditorInfo mEditorInfo;
@@ -81,7 +84,8 @@ public final class KeyboardId {
mLocale = SubtypeLocale.getSubtypeLocale(mSubtype);
mDeviceFormFactor = params.mDeviceFormFactor;
mOrientation = params.mOrientation;
- mWidth = params.mWidth;
+ mWidth = params.mKeyboardWidth;
+ mHeight = params.mKeyboardHeight;
mMode = params.mMode;
mElementId = elementId;
mEditorInfo = params.mEditorInfo;
@@ -108,6 +112,7 @@ public final class KeyboardId {
id.mElementId,
id.mMode,
id.mWidth,
+ id.mHeight,
id.passwordInput(),
id.mClobberSettingsKey,
id.mShortcutKeyEnabled,
@@ -130,6 +135,7 @@ public final class KeyboardId {
&& other.mElementId == mElementId
&& other.mMode == mMode
&& other.mWidth == mWidth
+ && other.mHeight == mHeight
&& other.passwordInput() == passwordInput()
&& other.mClobberSettingsKey == mClobberSettingsKey
&& other.mShortcutKeyEnabled == mShortcutKeyEnabled
@@ -187,11 +193,13 @@ public final class KeyboardId {
@Override
public String toString() {
- return String.format("[%s %s:%s %s-%s:%d %s %s %s%s%s%s%s%s%s%s%s]",
+ final String orientation = (mOrientation == Configuration.ORIENTATION_PORTRAIT)
+ ? "port" : "land";
+ return String.format("[%s %s:%s %s-%s:%dx%d %s %s %s%s%s%s%s%s%s%s%s]",
elementIdToName(mElementId),
mLocale,
mSubtype.getExtraValueOf(KEYBOARD_LAYOUT_SET),
- deviceFormFactor(mDeviceFormFactor), (mOrientation == 1 ? "port" : "land"), mWidth,
+ deviceFormFactor(mDeviceFormFactor), orientation, mWidth, mHeight,
modeName(mMode),
imeAction(),
(navigateNext() ? "navigateNext" : ""),
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java
index f060ad083..fd9edec70 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java
@@ -28,6 +28,7 @@ import android.content.res.Resources;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
import android.text.InputType;
+import android.text.TextUtils;
import android.util.Log;
import android.util.SparseArray;
import android.util.Xml;
@@ -44,6 +45,7 @@ import com.android.inputmethod.latin.InputAttributes;
import com.android.inputmethod.latin.InputTypeUtils;
import com.android.inputmethod.latin.LatinImeLogger;
import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.ResourceUtils;
import com.android.inputmethod.latin.SubtypeLocale;
import com.android.inputmethod.latin.SubtypeSwitcher;
import com.android.inputmethod.latin.XmlParseUtils;
@@ -106,7 +108,8 @@ public final class KeyboardLayoutSet {
InputMethodSubtype mSubtype;
int mDeviceFormFactor;
int mOrientation;
- int mWidth;
+ int mKeyboardWidth;
+ int mKeyboardHeight;
// Sparse array of KeyboardLayoutSet element parameters indexed by element's id.
final SparseArray<ElementParams> mKeyboardLayoutSetElementIdToParamsMap =
CollectionUtils.newSparseArray();
@@ -214,15 +217,43 @@ public final class KeyboardLayoutSet {
mPackageName, NO_SETTINGS_KEY, mEditorInfo);
}
- public Builder setScreenGeometry(final int deviceFormFactor, final int orientation,
- final int widthPixels) {
+ public Builder setScreenGeometry(final int deviceFormFactor, final int widthPixels,
+ final int heightPixels) {
final Params params = mParams;
params.mDeviceFormFactor = deviceFormFactor;
- params.mOrientation = orientation;
- params.mWidth = widthPixels;
+ params.mOrientation = (heightPixels > widthPixels)
+ ? Configuration.ORIENTATION_PORTRAIT : Configuration.ORIENTATION_LANDSCAPE;
+ setDefaultKeyboardSize(widthPixels, heightPixels);
return this;
}
+ private void setDefaultKeyboardSize(final int widthPixels, final int heightPixels) {
+ final String keyboardHeightString = ResourceUtils.getDeviceOverrideValue(
+ mResources, R.array.keyboard_heights);
+ final float keyboardHeight;
+ if (TextUtils.isEmpty(keyboardHeightString)) {
+ keyboardHeight = mResources.getDimension(R.dimen.keyboardHeight);
+ } else {
+ keyboardHeight = Float.parseFloat(keyboardHeightString)
+ * mResources.getDisplayMetrics().density;
+ }
+ final float maxKeyboardHeight = mResources.getFraction(
+ R.fraction.maxKeyboardHeight, heightPixels, heightPixels);
+ float minKeyboardHeight = mResources.getFraction(
+ R.fraction.minKeyboardHeight, heightPixels, heightPixels);
+ if (minKeyboardHeight < 0.0f) {
+ // Specified fraction was negative, so it should be calculated against display
+ // width.
+ minKeyboardHeight = -mResources.getFraction(
+ R.fraction.minKeyboardHeight, widthPixels, widthPixels);
+ }
+ // Keyboard height will not exceed maxKeyboardHeight and will not be less than
+ // minKeyboardHeight.
+ mParams.mKeyboardHeight = (int)Math.max(
+ Math.min(keyboardHeight, maxKeyboardHeight), minKeyboardHeight);
+ mParams.mKeyboardWidth = widthPixels;
+ }
+
public Builder setSubtype(final InputMethodSubtype subtype) {
final boolean asciiCapable = subtype.containsExtraValueKey(ASCII_CAPABLE);
@SuppressWarnings("deprecation")
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
index 30949aea8..d15f14f88 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
@@ -20,6 +20,7 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.preference.PreferenceManager;
+import android.util.DisplayMetrics;
import android.util.Log;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
@@ -140,8 +141,9 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder(
mThemeContext, editorInfo);
final Resources res = mThemeContext.getResources();
+ final DisplayMetrics dm = res.getDisplayMetrics();
builder.setScreenGeometry(res.getInteger(R.integer.config_device_form_factor),
- res.getConfiguration().orientation, res.getDisplayMetrics().widthPixels);
+ dm.widthPixels, dm.heightPixels);
builder.setSubtype(mSubtypeSwitcher.getCurrentSubtype());
builder.setOptions(
settingsValues.isVoiceKeyEnabled(editorInfo),
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java
index 04b8ecba1..e087a4565 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java
@@ -20,9 +20,7 @@ import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
-import android.text.TextUtils;
import android.util.AttributeSet;
-import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
import android.util.Xml;
@@ -138,7 +136,6 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
protected final KP mParams;
protected final Context mContext;
protected final Resources mResources;
- private final DisplayMetrics mDisplayMetrics;
private int mCurrentY = 0;
private KeyboardRow mCurrentRow = null;
@@ -150,7 +147,6 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
mContext = context;
final Resources res = context.getResources();
mResources = res;
- mDisplayMetrics = res.getDisplayMetrics();
mParams = params;
@@ -232,63 +228,43 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
}
private void parseKeyboardAttributes(final XmlPullParser parser) {
- final int displayWidth = mDisplayMetrics.widthPixels;
final TypedArray keyboardAttr = mContext.obtainStyledAttributes(
Xml.asAttributeSet(parser), R.styleable.Keyboard, R.attr.keyboardStyle,
R.style.Keyboard);
final TypedArray keyAttr = mResources.obtainAttributes(Xml.asAttributeSet(parser),
R.styleable.Keyboard_Key);
try {
- final int displayHeight = mDisplayMetrics.heightPixels;
- final String keyboardHeightString = ResourceUtils.getDeviceOverrideValue(
- mResources, R.array.keyboard_heights);
- final float keyboardHeight;
- if (TextUtils.isEmpty(keyboardHeightString)) {
- keyboardHeight = keyboardAttr.getDimension(
- R.styleable.Keyboard_keyboardHeight, displayHeight / 2);
- } else {
- keyboardHeight = Float.parseFloat(keyboardHeightString)
- * mDisplayMetrics.density;
- }
- final float maxKeyboardHeight = ResourceUtils.getDimensionOrFraction(keyboardAttr,
- R.styleable.Keyboard_maxKeyboardHeight, displayHeight, displayHeight / 2);
- float minKeyboardHeight = ResourceUtils.getDimensionOrFraction(keyboardAttr,
- R.styleable.Keyboard_minKeyboardHeight, displayHeight, displayHeight / 2);
- if (minKeyboardHeight < 0) {
- // Specified fraction was negative, so it should be calculated against display
- // width.
- minKeyboardHeight = -ResourceUtils.getDimensionOrFraction(keyboardAttr,
- R.styleable.Keyboard_minKeyboardHeight, displayWidth, displayWidth / 2);
- }
final KeyboardParams params = mParams;
- // Keyboard height will not exceed maxKeyboardHeight and will not be less than
- // minKeyboardHeight.
- params.mOccupiedHeight = (int)Math.max(
- Math.min(keyboardHeight, maxKeyboardHeight), minKeyboardHeight);
- params.mOccupiedWidth = params.mId.mWidth;
- params.mTopPadding = (int)ResourceUtils.getDimensionOrFraction(keyboardAttr,
- R.styleable.Keyboard_keyboardTopPadding, params.mOccupiedHeight, 0);
- params.mBottomPadding = (int)ResourceUtils.getDimensionOrFraction(keyboardAttr,
- R.styleable.Keyboard_keyboardBottomPadding, params.mOccupiedHeight, 0);
- params.mHorizontalEdgesPadding = (int)ResourceUtils.getDimensionOrFraction(
- keyboardAttr,
- R.styleable.Keyboard_keyboardHorizontalEdgesPadding,
- mParams.mOccupiedWidth, 0);
-
- params.mBaseWidth = params.mOccupiedWidth - params.mHorizontalEdgesPadding * 2
- - params.mHorizontalCenterPadding;
- params.mDefaultKeyWidth = (int)ResourceUtils.getDimensionOrFraction(keyAttr,
- R.styleable.Keyboard_Key_keyWidth, params.mBaseWidth,
- params.mBaseWidth / DEFAULT_KEYBOARD_COLUMNS);
- params.mHorizontalGap = (int)ResourceUtils.getDimensionOrFraction(keyboardAttr,
- R.styleable.Keyboard_horizontalGap, params.mBaseWidth, 0);
- params.mVerticalGap = (int)ResourceUtils.getDimensionOrFraction(keyboardAttr,
- R.styleable.Keyboard_verticalGap, params.mOccupiedHeight, 0);
- params.mBaseHeight = params.mOccupiedHeight - params.mTopPadding
+ final int height = params.mId.mHeight;
+ final int width = params.mId.mWidth;
+ params.mOccupiedHeight = height;
+ params.mOccupiedWidth = width;
+ params.mTopPadding = (int)keyboardAttr.getFraction(
+ R.styleable.Keyboard_keyboardTopPadding, height, height, 0);
+ params.mBottomPadding = (int)keyboardAttr.getFraction(
+ R.styleable.Keyboard_keyboardBottomPadding, height, height, 0);
+ params.mLeftPadding = (int)keyboardAttr.getFraction(
+ R.styleable.Keyboard_keyboardLeftPadding, width, width, 0);
+ params.mRightPadding = (int)keyboardAttr.getFraction(
+ R.styleable.Keyboard_keyboardRightPadding, width, width, 0);
+
+ final int baseWidth =
+ params.mOccupiedWidth - params.mLeftPadding - params.mRightPadding;
+ params.mBaseWidth = baseWidth;
+ params.mDefaultKeyWidth = (int)keyAttr.getFraction(R.styleable.Keyboard_Key_keyWidth,
+ baseWidth, baseWidth, baseWidth / DEFAULT_KEYBOARD_COLUMNS);
+ params.mHorizontalGap = (int)keyboardAttr.getFraction(
+ R.styleable.Keyboard_horizontalGap, baseWidth, baseWidth, 0);
+ // TODO: Fix keyboard geometry calculation clearer. Historically vertical gap between
+ // rows are determined based on the entire keyboard height including top and bottom
+ // paddings.
+ params.mVerticalGap = (int)keyboardAttr.getFraction(
+ R.styleable.Keyboard_verticalGap, height, height, 0);
+ final int baseHeight = params.mOccupiedHeight - params.mTopPadding
- params.mBottomPadding + params.mVerticalGap;
+ params.mBaseHeight = baseHeight;
params.mDefaultRowHeight = (int)ResourceUtils.getDimensionOrFraction(keyboardAttr,
- R.styleable.Keyboard_rowHeight, params.mBaseHeight,
- params.mBaseHeight / DEFAULT_KEYBOARD_ROWS);
+ R.styleable.Keyboard_rowHeight, baseHeight, baseHeight / DEFAULT_KEYBOARD_ROWS);
params.mKeyVisualAttributes = KeyVisualAttributes.newInstance(keyAttr);
@@ -766,7 +742,7 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
}
private void startRow(final KeyboardRow row) {
- addEdgeSpace(mParams.mHorizontalEdgesPadding, row);
+ addEdgeSpace(mParams.mLeftPadding, row);
mCurrentRow = row;
mLeftEdge = true;
mRightEdgeKey = null;
@@ -780,7 +756,7 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
mRightEdgeKey.markAsRightEdge(mParams);
mRightEdgeKey = null;
}
- addEdgeSpace(mParams.mHorizontalEdgesPadding, row);
+ addEdgeSpace(mParams.mRightPadding, row);
mCurrentY += row.mRowHeight;
mCurrentRow = null;
mTopEdge = false;
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardParams.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardParams.java
index e13dbe5d0..15eb690e1 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardParams.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardParams.java
@@ -42,8 +42,8 @@ public class KeyboardParams {
public int mTopPadding;
public int mBottomPadding;
- public int mHorizontalEdgesPadding;
- public int mHorizontalCenterPadding;
+ public int mLeftPadding;
+ public int mRightPadding;
public KeyVisualAttributes mKeyVisualAttributes;
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardRow.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardRow.java
index 22780205d..855f65507 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardRow.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardRow.java
@@ -54,17 +54,16 @@ public final class KeyboardRow {
public KeyboardRow(final Resources res, final KeyboardParams params, final XmlPullParser parser,
final int y) {
mParams = params;
- TypedArray keyboardAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
+ final TypedArray keyboardAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
R.styleable.Keyboard);
mRowHeight = (int)ResourceUtils.getDimensionOrFraction(keyboardAttr,
R.styleable.Keyboard_rowHeight,
params.mBaseHeight, params.mDefaultRowHeight);
keyboardAttr.recycle();
- TypedArray keyAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
+ final TypedArray keyAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
R.styleable.Keyboard_Key);
- mDefaultKeyWidth = ResourceUtils.getDimensionOrFraction(keyAttr,
- R.styleable.Keyboard_Key_keyWidth,
- params.mBaseWidth, params.mDefaultKeyWidth);
+ mDefaultKeyWidth = keyAttr.getFraction(R.styleable.Keyboard_Key_keyWidth,
+ params.mBaseWidth, params.mBaseWidth, params.mDefaultKeyWidth);
mDefaultBackgroundType = keyAttr.getInt(R.styleable.Keyboard_Key_backgroundType,
Key.BACKGROUND_TYPE_NORMAL);
keyAttr.recycle();
@@ -112,20 +111,19 @@ public final class KeyboardRow {
}
public float getKeyX(final TypedArray keyAttr) {
- final int keyboardRightEdge = mParams.mOccupiedWidth
- - mParams.mHorizontalEdgesPadding;
if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyXPos)) {
- final float keyXPos = ResourceUtils.getDimensionOrFraction(keyAttr,
- R.styleable.Keyboard_Key_keyXPos, mParams.mBaseWidth, 0);
+ final float keyXPos = keyAttr.getFraction(R.styleable.Keyboard_Key_keyXPos,
+ mParams.mBaseWidth, mParams.mBaseWidth, 0);
if (keyXPos < 0) {
// If keyXPos is negative, the actual x-coordinate will be
// keyboardWidth + keyXPos.
// keyXPos shouldn't be less than mCurrentX because drawable area for this
// key starts at mCurrentX. Or, this key will overlaps the adjacent key on
// its left hand side.
+ final int keyboardRightEdge = mParams.mOccupiedWidth - mParams.mRightPadding;
return Math.max(keyXPos + keyboardRightEdge, mCurrentX);
} else {
- return keyXPos + mParams.mHorizontalEdgesPadding;
+ return keyXPos + mParams.mLeftPadding;
}
}
return mCurrentX;
@@ -140,15 +138,13 @@ public final class KeyboardRow {
R.styleable.Keyboard_Key_keyWidth, KEYWIDTH_NOT_ENUM);
switch (widthType) {
case KEYWIDTH_FILL_RIGHT:
- final int keyboardRightEdge =
- mParams.mOccupiedWidth - mParams.mHorizontalEdgesPadding;
// If keyWidth is fillRight, the actual key width will be determined to fill
// out the area up to the right edge of the keyboard.
+ final int keyboardRightEdge = mParams.mOccupiedWidth - mParams.mRightPadding;
return keyboardRightEdge - keyXPos;
default: // KEYWIDTH_NOT_ENUM
- return ResourceUtils.getDimensionOrFraction(keyAttr,
- R.styleable.Keyboard_Key_keyWidth,
- mParams.mBaseWidth, mDefaultKeyWidth);
+ return keyAttr.getFraction(R.styleable.Keyboard_Key_keyWidth,
+ mParams.mBaseWidth, mParams.mBaseWidth, mDefaultKeyWidth);
}
}
}
diff --git a/java/src/com/android/inputmethod/latin/AssetFileAddress.java b/java/src/com/android/inputmethod/latin/AssetFileAddress.java
index 16296f0e2..47c750f54 100644
--- a/java/src/com/android/inputmethod/latin/AssetFileAddress.java
+++ b/java/src/com/android/inputmethod/latin/AssetFileAddress.java
@@ -35,11 +35,14 @@ final class AssetFileAddress {
mLength = length;
}
+ public static AssetFileAddress makeFromFile(final File file) {
+ if (!file.isFile()) return null;
+ return new AssetFileAddress(file.getAbsolutePath(), 0L, file.length());
+ }
+
public static AssetFileAddress makeFromFileName(final String filename) {
if (null == filename) return null;
- final File f = new File(filename);
- if (!f.isFile()) return null;
- return new AssetFileAddress(filename, 0l, f.length());
+ return makeFromFile(new File(filename));
}
public static AssetFileAddress makeFromFileNameAndOffset(final String filename,
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
index d0bd01f6b..443ffa2e9 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
@@ -149,7 +149,7 @@ public final class BinaryDictionaryFileDumper {
final int MODE_MAX = NONE;
final Uri.Builder wordListUriBuilder = getProviderUriBuilder(id);
- final String finalFileName = BinaryDictionaryGetter.getCacheFileName(id, locale, context);
+ final String finalFileName = DictionaryInfoUtils.getCacheFileName(id, locale, context);
String tempFileName;
try {
tempFileName = BinaryDictionaryGetter.getTempFileName(id, context);
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
index 22b5cd55c..5da0f1be8 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
@@ -66,109 +66,11 @@ final class BinaryDictionaryGetter {
private BinaryDictionaryGetter() {}
/**
- * Returns whether we may want to use this character as part of a file name.
- *
- * This basically only accepts ascii letters and numbers, and rejects everything else.
- */
- private static boolean isFileNameCharacter(int codePoint) {
- if (codePoint >= 0x30 && codePoint <= 0x39) return true; // Digit
- if (codePoint >= 0x41 && codePoint <= 0x5A) return true; // Uppercase
- if (codePoint >= 0x61 && codePoint <= 0x7A) return true; // Lowercase
- return codePoint == '_'; // Underscore
- }
-
- /**
- * Escapes a string for any characters that may be suspicious for a file or directory name.
- *
- * Concretely this does a sort of URL-encoding except it will encode everything that's not
- * alphanumeric or underscore. (true URL-encoding leaves alone characters like '*', which
- * we cannot allow here)
- */
- // TODO: create a unit test for this method
- private static String replaceFileNameDangerousCharacters(final String name) {
- // This assumes '%' is fully available as a non-separator, normal
- // character in a file name. This is probably true for all file systems.
- final StringBuilder sb = new StringBuilder();
- final int nameLength = name.length();
- for (int i = 0; i < nameLength; i = name.offsetByCodePoints(i, 1)) {
- final int codePoint = name.codePointAt(i);
- if (isFileNameCharacter(codePoint)) {
- sb.appendCodePoint(codePoint);
- } else {
- // 6 digits - unicode is limited to 21 bits
- sb.append(String.format((Locale)null, "%%%1$06x", codePoint));
- }
- }
- return sb.toString();
- }
-
- /**
- * Reverse escaping done by replaceFileNameDangerousCharacters.
- */
- private static String getWordListIdFromFileName(final String fname) {
- final StringBuilder sb = new StringBuilder();
- final int fnameLength = fname.length();
- for (int i = 0; i < fnameLength; i = fname.offsetByCodePoints(i, 1)) {
- final int codePoint = fname.codePointAt(i);
- if ('%' != codePoint) {
- sb.appendCodePoint(codePoint);
- } else {
- final int encodedCodePoint = Integer.parseInt(fname.substring(i + 1, i + 7), 16);
- i += 6;
- sb.appendCodePoint(encodedCodePoint);
- }
- }
- return sb.toString();
- }
-
- /**
- * Helper method to get the top level cache directory.
- */
- private static String getWordListCacheDirectory(final Context context) {
- return context.getFilesDir() + File.separator + "dicts";
- }
-
- /**
- * Find out the cache directory associated with a specific locale.
- */
- private static String getCacheDirectoryForLocale(final String locale, final Context context) {
- final String relativeDirectoryName = replaceFileNameDangerousCharacters(locale);
- final String absoluteDirectoryName = getWordListCacheDirectory(context) + File.separator
- + relativeDirectoryName;
- final File directory = new File(absoluteDirectoryName);
- if (!directory.exists()) {
- if (!directory.mkdirs()) {
- Log.e(TAG, "Could not create the directory for locale" + locale);
- }
- }
- return absoluteDirectoryName;
- }
-
- /**
- * Generates a file name for the id and locale passed as an argument.
- *
- * In the current implementation the file name returned will always be unique for
- * any id/locale pair, but please do not expect that the id can be the same for
- * different dictionaries with different locales. An id should be unique for any
- * dictionary.
- * The file name is pretty much an URL-encoded version of the id inside a directory
- * named like the locale, except it will also escape characters that look dangerous
- * to some file systems.
- * @param id the id of the dictionary for which to get a file name
- * @param locale the locale for which to get the file name as a string
- * @param context the context to use for getting the directory
- * @return the name of the file to be created
- */
- public static String getCacheFileName(String id, String locale, Context context) {
- final String fileName = replaceFileNameDangerousCharacters(id);
- return getCacheDirectoryForLocale(locale, context) + File.separator + fileName;
- }
-
- /**
* Generates a unique temporary file name in the app cache directory.
*/
public static String getTempFileName(String id, Context context) throws IOException {
- return File.createTempFile(replaceFileNameDangerousCharacters(id), null).getAbsolutePath();
+ return File.createTempFile(DictionaryInfoUtils.replaceFileNameDangerousCharacters(id),
+ null).getAbsolutePath();
}
/**
@@ -222,27 +124,6 @@ final class BinaryDictionaryGetter {
}
/**
- * Helper method to the list of cache directories, one for each distinct locale.
- */
- private static File[] getCachedDirectoryList(final Context context) {
- return new File(getWordListCacheDirectory(context)).listFiles();
- }
-
- /**
- * Returns the category for a given file name.
- *
- * This parses the file name, extracts the category, and returns it. See
- * {@link #getMainDictId(Locale)} and {@link #isMainWordListId(String)}.
- * @return The category as a string or null if it can't be found in the file name.
- */
- private static String getCategoryFromFileName(final String fileName) {
- final String id = getWordListIdFromFileName(fileName);
- final String[] idArray = id.split(ID_CATEGORY_SEPARATOR);
- if (2 != idArray.length) return null;
- return idArray[0];
- }
-
- /**
* Utility class for the {@link #getCachedWordLists} method
*/
private static final class FileAndMatchLevel {
@@ -268,20 +149,21 @@ final class BinaryDictionaryGetter {
* @param context the context on which to open the files upon.
* @return an array of binary dictionary files, which may be empty but may not be null.
*/
- private static File[] getCachedWordLists(final String locale,
- final Context context) {
- final File[] directoryList = getCachedDirectoryList(context);
+ private static File[] getCachedWordLists(final String locale, final Context context) {
+ final File[] directoryList = DictionaryInfoUtils.getCachedDirectoryList(context);
if (null == directoryList) return EMPTY_FILE_ARRAY;
final HashMap<String, FileAndMatchLevel> cacheFiles = CollectionUtils.newHashMap();
for (File directory : directoryList) {
if (!directory.isDirectory()) continue;
- final String dirLocale = getWordListIdFromFileName(directory.getName());
+ final String dirLocale =
+ DictionaryInfoUtils.getWordListIdFromFileName(directory.getName());
final int matchLevel = LocaleUtils.getMatchLevel(dirLocale, locale);
if (LocaleUtils.isMatch(matchLevel)) {
final File[] wordLists = directory.listFiles();
if (null != wordLists) {
for (File wordList : wordLists) {
- final String category = getCategoryFromFileName(wordList.getName());
+ final String category =
+ DictionaryInfoUtils.getCategoryFromFileName(wordList.getName());
final FileAndMatchLevel currentBestMatch = cacheFiles.get(category);
if (null == currentBestMatch || currentBestMatch.mMatchLevel < matchLevel) {
cacheFiles.put(category, new FileAndMatchLevel(wordList, matchLevel));
@@ -310,7 +192,7 @@ final class BinaryDictionaryGetter {
final File fileToKeep) {
try {
final File canonicalFileToKeep = fileToKeep.getCanonicalFile();
- final File[] directoryList = getCachedDirectoryList(context);
+ final File[] directoryList = DictionaryInfoUtils.getCachedDirectoryList(context);
if (null == directoryList) return;
for (File directory : directoryList) {
// There is one directory per locale. See #getCachedDirectoryList
@@ -318,7 +200,8 @@ final class BinaryDictionaryGetter {
final File[] wordLists = directory.listFiles();
if (null == wordLists) continue;
for (File wordList : wordLists) {
- final String fileId = getWordListIdFromFileName(wordList.getName());
+ final String fileId =
+ DictionaryInfoUtils.getWordListIdFromFileName(wordList.getName());
if (fileId.equals(id)) {
if (!canonicalFileToKeep.equals(wordList.getCanonicalFile())) {
wordList.delete();
@@ -331,28 +214,6 @@ final class BinaryDictionaryGetter {
}
}
-
- /**
- * Returns the id associated with the main word list for a specified locale.
- *
- * Word lists stored in Android Keyboard's resources are referred to as the "main"
- * word lists. Since they can be updated like any other list, we need to assign a
- * unique ID to them. This ID is just the name of the language (locale-wise) they
- * are for, and this method returns this ID.
- */
- private static String getMainDictId(final Locale locale) {
- // This works because we don't include by default different dictionaries for
- // different countries. This actually needs to return the id that we would
- // like to use for word lists included in resources, and the following is okay.
- return MAIN_DICTIONARY_CATEGORY + ID_CATEGORY_SEPARATOR + locale.getLanguage().toString();
- }
-
- private static boolean isMainWordListId(final String id) {
- final String[] idArray = id.split(ID_CATEGORY_SEPARATOR);
- if (2 != idArray.length) return false;
- return MAIN_DICTIONARY_CATEGORY.equals(idArray[0]);
- }
-
// ## HACK ## we prevent usage of a dictionary before version 18 for English only. The reason
// for this is, since those do not include whitelist entries, the new code with an old version
// of the dictionary would lose whitelist functionality.
@@ -429,16 +290,16 @@ final class BinaryDictionaryGetter {
hasDefaultWordList);
}
final File[] cachedWordLists = getCachedWordLists(locale.toString(), context);
- final String mainDictId = getMainDictId(locale);
+ final String mainDictId = DictionaryInfoUtils.getMainDictId(locale);
final DictPackSettings dictPackSettings = new DictPackSettings(context);
boolean foundMainDict = false;
final ArrayList<AssetFileAddress> fileList = CollectionUtils.newArrayList();
// cachedWordLists may not be null, see doc for getCachedDictionaryList
for (final File f : cachedWordLists) {
- final String wordListId = getWordListIdFromFileName(f.getName());
+ final String wordListId = DictionaryInfoUtils.getWordListIdFromFileName(f.getName());
final boolean canUse = f.canRead() && hackCanUseDictionaryFile(locale, f);
- if (canUse && isMainWordListId(wordListId)) {
+ if (canUse && DictionaryInfoUtils.isMainWordListId(wordListId)) {
foundMainDict = true;
}
if (!dictPackSettings.isWordListActive(wordListId)) continue;
@@ -451,7 +312,7 @@ final class BinaryDictionaryGetter {
if (!foundMainDict && dictPackSettings.isWordListActive(mainDictId)) {
final int fallbackResId =
- DictionaryFactory.getMainDictionaryResourceId(context.getResources(), locale);
+ DictionaryInfoUtils.getMainDictionaryResourceId(context.getResources(), locale);
final AssetFileAddress fallbackAsset = loadFallbackResource(context, fallbackResId);
if (null != fallbackAsset) {
fileList.add(fallbackAsset);
diff --git a/java/src/com/android/inputmethod/latin/DictionaryFactory.java b/java/src/com/android/inputmethod/latin/DictionaryFactory.java
index 388ad6c59..40e51672a 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryFactory.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryFactory.java
@@ -31,9 +31,6 @@ import java.util.Locale;
*/
public final class DictionaryFactory {
private static final String TAG = DictionaryFactory.class.getSimpleName();
- // This class must be located in the same package as LatinIME.java.
- private static final String RESOURCE_PACKAGE_NAME =
- DictionaryFactory.class.getPackage().getName();
/**
* Initializes a main dictionary collection from a dictionary pack, with explicit flags.
@@ -96,8 +93,8 @@ public final class DictionaryFactory {
final Locale locale) {
AssetFileDescriptor afd = null;
try {
- final int resId =
- getMainDictionaryResourceIdIfAvailableForLocale(context.getResources(), locale);
+ final int resId = DictionaryInfoUtils.getMainDictionaryResourceIdIfAvailableForLocale(
+ context.getResources(), locale);
if (0 == resId) return null;
afd = context.getResources().openRawResourceFd(resId);
if (afd == null) {
@@ -154,47 +151,7 @@ public final class DictionaryFactory {
*/
public static boolean isDictionaryAvailable(Context context, Locale locale) {
final Resources res = context.getResources();
- return 0 != getMainDictionaryResourceIdIfAvailableForLocale(res, locale);
- }
-
- private static final String DEFAULT_MAIN_DICT = "main";
- private static final String MAIN_DICT_PREFIX = "main_";
-
- /**
- * Helper method to return a dictionary res id for a locale, or 0 if none.
- * @param locale dictionary locale
- * @return main dictionary resource id
- */
- private static int getMainDictionaryResourceIdIfAvailableForLocale(final Resources res,
- final Locale locale) {
- int resId;
- // Try to find main_language_country dictionary.
- if (!locale.getCountry().isEmpty()) {
- final String dictLanguageCountry = MAIN_DICT_PREFIX + locale.toString().toLowerCase();
- if ((resId = res.getIdentifier(
- dictLanguageCountry, "raw", RESOURCE_PACKAGE_NAME)) != 0) {
- return resId;
- }
- }
-
- // Try to find main_language dictionary.
- final String dictLanguage = MAIN_DICT_PREFIX + locale.getLanguage();
- if ((resId = res.getIdentifier(dictLanguage, "raw", RESOURCE_PACKAGE_NAME)) != 0) {
- return resId;
- }
-
- // Not found, return 0
- return 0;
- }
-
- /**
- * Returns a main dictionary resource id
- * @param locale dictionary locale
- * @return main dictionary resource id
- */
- public static int getMainDictionaryResourceId(final Resources res, final Locale locale) {
- int resourceId = getMainDictionaryResourceIdIfAvailableForLocale(res, locale);
- if (0 != resourceId) return resourceId;
- return res.getIdentifier(DEFAULT_MAIN_DICT, "raw", RESOURCE_PACKAGE_NAME);
+ return 0 != DictionaryInfoUtils.getMainDictionaryResourceIdIfAvailableForLocale(
+ res, locale);
}
}
diff --git a/java/src/com/android/inputmethod/latin/DictionaryInfoUtils.java b/java/src/com/android/inputmethod/latin/DictionaryInfoUtils.java
new file mode 100644
index 000000000..c676bf1b9
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/DictionaryInfoUtils.java
@@ -0,0 +1,245 @@
+/*
+ * 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.Context;
+import android.content.res.Resources;
+import android.util.Log;
+
+import com.android.inputmethod.latin.makedict.BinaryDictIOUtils;
+import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader;
+import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Locale;
+
+/**
+ * This class encapsulates the logic for the Latin-IME side of dictionary information management.
+ */
+public class DictionaryInfoUtils {
+ private static final String TAG = DictionaryInfoUtils.class.getSimpleName();
+ // This class must be located in the same package as LatinIME.java.
+ private static final String RESOURCE_PACKAGE_NAME =
+ DictionaryInfoUtils.class.getPackage().getName();
+ private static final String DEFAULT_MAIN_DICT = "main";
+ private static final String MAIN_DICT_PREFIX = "main_";
+ // 6 digits - unicode is limited to 21 bits
+ private static final int MAX_HEX_DIGITS_FOR_CODEPOINT = 6;
+
+ private DictionaryInfoUtils() {
+ // Private constructor to forbid instantation of this helper class.
+ }
+
+ /**
+ * Returns whether we may want to use this character as part of a file name.
+ *
+ * This basically only accepts ascii letters and numbers, and rejects everything else.
+ */
+ private static boolean isFileNameCharacter(int codePoint) {
+ if (codePoint >= 0x30 && codePoint <= 0x39) return true; // Digit
+ if (codePoint >= 0x41 && codePoint <= 0x5A) return true; // Uppercase
+ if (codePoint >= 0x61 && codePoint <= 0x7A) return true; // Lowercase
+ return codePoint == '_'; // Underscore
+ }
+
+ /**
+ * Escapes a string for any characters that may be suspicious for a file or directory name.
+ *
+ * Concretely this does a sort of URL-encoding except it will encode everything that's not
+ * alphanumeric or underscore. (true URL-encoding leaves alone characters like '*', which
+ * we cannot allow here)
+ */
+ // TODO: create a unit test for this method
+ public static String replaceFileNameDangerousCharacters(final String name) {
+ // This assumes '%' is fully available as a non-separator, normal
+ // character in a file name. This is probably true for all file systems.
+ final StringBuilder sb = new StringBuilder();
+ final int nameLength = name.length();
+ for (int i = 0; i < nameLength; i = name.offsetByCodePoints(i, 1)) {
+ final int codePoint = name.codePointAt(i);
+ if (DictionaryInfoUtils.isFileNameCharacter(codePoint)) {
+ sb.appendCodePoint(codePoint);
+ } else {
+ sb.append(String.format((Locale)null, "%%%1$0" + MAX_HEX_DIGITS_FOR_CODEPOINT + "x",
+ codePoint));
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Helper method to get the top level cache directory.
+ */
+ private static String getWordListCacheDirectory(final Context context) {
+ return context.getFilesDir() + File.separator + "dicts";
+ }
+
+ /**
+ * Reverse escaping done by replaceFileNameDangerousCharacters.
+ */
+ public static String getWordListIdFromFileName(final String fname) {
+ final StringBuilder sb = new StringBuilder();
+ final int fnameLength = fname.length();
+ for (int i = 0; i < fnameLength; i = fname.offsetByCodePoints(i, 1)) {
+ final int codePoint = fname.codePointAt(i);
+ if ('%' != codePoint) {
+ sb.appendCodePoint(codePoint);
+ } else {
+ // + 1 to pass the % sign
+ final int encodedCodePoint = Integer.parseInt(
+ fname.substring(i + 1, i + 1 + MAX_HEX_DIGITS_FOR_CODEPOINT), 16);
+ i += MAX_HEX_DIGITS_FOR_CODEPOINT;
+ sb.appendCodePoint(encodedCodePoint);
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Helper method to the list of cache directories, one for each distinct locale.
+ */
+ public static File[] getCachedDirectoryList(final Context context) {
+ return new File(DictionaryInfoUtils.getWordListCacheDirectory(context)).listFiles();
+ }
+
+ /**
+ * Returns the category for a given file name.
+ *
+ * This parses the file name, extracts the category, and returns it. See
+ * {@link #getMainDictId(Locale)} and {@link #isMainWordListId(String)}.
+ * @return The category as a string or null if it can't be found in the file name.
+ */
+ public static String getCategoryFromFileName(final String fileName) {
+ final String id = getWordListIdFromFileName(fileName);
+ final String[] idArray = id.split(BinaryDictionaryGetter.ID_CATEGORY_SEPARATOR);
+ // An id is supposed to be in format category:locale, so splitting on the separator
+ // should yield a 2-elements array
+ if (2 != idArray.length) return null;
+ return idArray[0];
+ }
+
+ /**
+ * Find out the cache directory associated with a specific locale.
+ */
+ private static String getCacheDirectoryForLocale(final String locale, final Context context) {
+ final String relativeDirectoryName = replaceFileNameDangerousCharacters(locale);
+ final String absoluteDirectoryName = getWordListCacheDirectory(context) + File.separator
+ + relativeDirectoryName;
+ final File directory = new File(absoluteDirectoryName);
+ if (!directory.exists()) {
+ if (!directory.mkdirs()) {
+ Log.e(TAG, "Could not create the directory for locale" + locale);
+ }
+ }
+ return absoluteDirectoryName;
+ }
+
+ /**
+ * Generates a file name for the id and locale passed as an argument.
+ *
+ * In the current implementation the file name returned will always be unique for
+ * any id/locale pair, but please do not expect that the id can be the same for
+ * different dictionaries with different locales. An id should be unique for any
+ * dictionary.
+ * The file name is pretty much an URL-encoded version of the id inside a directory
+ * named like the locale, except it will also escape characters that look dangerous
+ * to some file systems.
+ * @param id the id of the dictionary for which to get a file name
+ * @param locale the locale for which to get the file name as a string
+ * @param context the context to use for getting the directory
+ * @return the name of the file to be created
+ */
+ public static String getCacheFileName(String id, String locale, Context context) {
+ final String fileName = replaceFileNameDangerousCharacters(id);
+ return getCacheDirectoryForLocale(locale, context) + File.separator + fileName;
+ }
+
+ public static boolean isMainWordListId(final String id) {
+ final String[] idArray = id.split(BinaryDictionaryGetter.ID_CATEGORY_SEPARATOR);
+ // An id is supposed to be in format category:locale, so splitting on the separator
+ // should yield a 2-elements array
+ if (2 != idArray.length) return false;
+ return BinaryDictionaryGetter.MAIN_DICTIONARY_CATEGORY.equals(idArray[0]);
+ }
+
+ /**
+ * Helper method to return a dictionary res id for a locale, or 0 if none.
+ * @param locale dictionary locale
+ * @return main dictionary resource id
+ */
+ public static int getMainDictionaryResourceIdIfAvailableForLocale(final Resources res,
+ final Locale locale) {
+ int resId;
+ // Try to find main_language_country dictionary.
+ if (!locale.getCountry().isEmpty()) {
+ final String dictLanguageCountry =
+ MAIN_DICT_PREFIX + locale.toString().toLowerCase(Locale.ROOT);
+ if ((resId = res.getIdentifier(
+ dictLanguageCountry, "raw", RESOURCE_PACKAGE_NAME)) != 0) {
+ return resId;
+ }
+ }
+
+ // Try to find main_language dictionary.
+ final String dictLanguage = MAIN_DICT_PREFIX + locale.getLanguage();
+ if ((resId = res.getIdentifier(dictLanguage, "raw", RESOURCE_PACKAGE_NAME)) != 0) {
+ return resId;
+ }
+
+ // Not found, return 0
+ return 0;
+ }
+
+ /**
+ * Returns a main dictionary resource id
+ * @param locale dictionary locale
+ * @return main dictionary resource id
+ */
+ public static int getMainDictionaryResourceId(final Resources res, final Locale locale) {
+ int resourceId = getMainDictionaryResourceIdIfAvailableForLocale(res, locale);
+ if (0 != resourceId) return resourceId;
+ return res.getIdentifier(DEFAULT_MAIN_DICT, "raw", RESOURCE_PACKAGE_NAME);
+ }
+
+ /**
+ * Returns the id associated with the main word list for a specified locale.
+ *
+ * Word lists stored in Android Keyboard's resources are referred to as the "main"
+ * word lists. Since they can be updated like any other list, we need to assign a
+ * unique ID to them. This ID is just the name of the language (locale-wise) they
+ * are for, and this method returns this ID.
+ */
+ public static String getMainDictId(final Locale locale) {
+ // This works because we don't include by default different dictionaries for
+ // different countries. This actually needs to return the id that we would
+ // like to use for word lists included in resources, and the following is okay.
+ return BinaryDictionaryGetter.MAIN_DICTIONARY_CATEGORY +
+ BinaryDictionaryGetter.ID_CATEGORY_SEPARATOR + locale.getLanguage().toString();
+ }
+
+ public static FileHeader getDictionaryFileHeaderOrNull(final File file) {
+ try {
+ final FileHeader header = BinaryDictIOUtils.getDictionaryFileHeader(file);
+ return header;
+ } catch (UnsupportedFormatException e) {
+ return null;
+ } catch (IOException e) {
+ return null;
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/ExternalDictionaryGetterForDebug.java b/java/src/com/android/inputmethod/latin/ExternalDictionaryGetterForDebug.java
index 6e5a37c72..d9e4bb63d 100644
--- a/java/src/com/android/inputmethod/latin/ExternalDictionaryGetterForDebug.java
+++ b/java/src/com/android/inputmethod/latin/ExternalDictionaryGetterForDebug.java
@@ -21,11 +21,8 @@ import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.os.Environment;
-import android.util.Log;
-import com.android.inputmethod.latin.makedict.BinaryDictIOUtils;
import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader;
-import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
@@ -44,22 +41,11 @@ public class ExternalDictionaryGetterForDebug {
+ "/Download";
private static final String DICTIONARY_LOCALE_ATTRIBUTE = "locale";
- private static FileHeader getDictionaryFileHeaderOrNull(final File file) {
- try {
- final FileHeader header = BinaryDictIOUtils.getDictionaryFileHeader(file);
- return header;
- } catch (UnsupportedFormatException e) {
- return null;
- } catch (IOException e) {
- return null;
- }
- }
-
private static String[] findDictionariesInTheDownloadedFolder() {
final File[] files = new File(SOURCE_FOLDER).listFiles();
final ArrayList<String> eligibleList = CollectionUtils.newArrayList();
for (File f : files) {
- final FileHeader header = getDictionaryFileHeaderOrNull(f);
+ final FileHeader header = DictionaryInfoUtils.getDictionaryFileHeaderOrNull(f);
if (null == header) continue;
eligibleList.add(f.getName());
}
@@ -102,7 +88,7 @@ public class ExternalDictionaryGetterForDebug {
private static void askInstallFile(final Context context, final String fileName) {
final File file = new File(SOURCE_FOLDER, fileName.toString());
- final FileHeader header = getDictionaryFileHeaderOrNull(file);
+ final FileHeader header = DictionaryInfoUtils.getDictionaryFileHeaderOrNull(file);
final StringBuilder message = new StringBuilder();
final String locale =
header.mDictionaryOptions.mAttributes.get(DICTIONARY_LOCALE_ATTRIBUTE);
@@ -143,7 +129,7 @@ public class ExternalDictionaryGetterForDebug {
final String id = BinaryDictionaryGetter.MAIN_DICTIONARY_CATEGORY
+ BinaryDictionaryGetter.ID_CATEGORY_SEPARATOR + locale;
final String finalFileName =
- BinaryDictionaryGetter.getCacheFileName(id, locale, context);
+ DictionaryInfoUtils.getCacheFileName(id, locale, context);
final String tempFileName = BinaryDictionaryGetter.getTempFileName(id, context);
tempFile = new File(tempFileName);
tempFile.delete();
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index d6487cb0c..08217326a 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -428,7 +428,7 @@ public final class LatinIME extends InputMethodService implements KeyboardAction
initSuggest();
if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.getInstance().init(this);
+ ResearchLogger.getInstance().init(this, mKeyboardSwitcher);
}
mDisplayOrientation = getResources().getConfiguration().orientation;
diff --git a/java/src/com/android/inputmethod/research/FeedbackActivity.java b/java/src/com/android/inputmethod/research/FeedbackActivity.java
index f66d55bdd..b985fda21 100644
--- a/java/src/com/android/inputmethod/research/FeedbackActivity.java
+++ b/java/src/com/android/inputmethod/research/FeedbackActivity.java
@@ -28,25 +28,10 @@ public class FeedbackActivity extends Activity {
super.onCreate(savedInstanceState);
setContentView(R.layout.research_feedback_activity);
final FeedbackLayout layout = (FeedbackLayout) findViewById(R.id.research_feedback_layout);
- final CheckBox checkbox = (CheckBox) findViewById(R.id.research_feedback_include_history);
- final CharSequence cs = checkbox.getText();
- final String actualString = String.format(cs.toString(),
- ResearchLogger.FEEDBACK_WORD_BUFFER_SIZE);
- checkbox.setText(actualString);
layout.setActivity(this);
}
@Override
- protected void onResume() {
- super.onResume();
- }
-
- @Override
- protected void onPause() {
- super.onPause();
- }
-
- @Override
public void onBackPressed() {
ResearchLogger.getInstance().onLeavingSendFeedbackDialog();
super.onBackPressed();
diff --git a/java/src/com/android/inputmethod/research/FeedbackFragment.java b/java/src/com/android/inputmethod/research/FeedbackFragment.java
index fee61a923..11a833a85 100644
--- a/java/src/com/android/inputmethod/research/FeedbackFragment.java
+++ b/java/src/com/android/inputmethod/research/FeedbackFragment.java
@@ -20,6 +20,7 @@ import android.app.Activity;
import android.app.Fragment;
import android.os.Bundle;
import android.text.Editable;
+import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
@@ -30,10 +31,18 @@ import android.widget.EditText;
import com.android.inputmethod.latin.R;
-public class FeedbackFragment extends Fragment {
+public class FeedbackFragment extends Fragment implements OnClickListener {
+ private static final String TAG = FeedbackFragment.class.getSimpleName();
+
+ private static final String KEY_FEEDBACK_STRING = "FeedbackString";
+ private static final String KEY_INCLUDE_ACCOUNT_NAME = "IncludeAccountName";
+ public static final String KEY_HAS_USER_RECORDING = "HasRecording";
+
private EditText mEditText;
- private CheckBox mIncludingHistoryCheckBox;
private CheckBox mIncludingAccountNameCheckBox;
+ private CheckBox mIncludingUserRecordingCheckBox;
+ private Button mSendButton;
+ private Button mCancelButton;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
@@ -41,39 +50,96 @@ public class FeedbackFragment extends Fragment {
final View view = inflater.inflate(R.layout.research_feedback_fragment_layout, container,
false);
mEditText = (EditText) view.findViewById(R.id.research_feedback_contents);
- mIncludingHistoryCheckBox = (CheckBox) view.findViewById(
- R.id.research_feedback_include_history);
+ mEditText.requestFocus();
mIncludingAccountNameCheckBox = (CheckBox) view.findViewById(
R.id.research_feedback_include_account_name);
+ mIncludingUserRecordingCheckBox = (CheckBox) view.findViewById(
+ R.id.research_feedback_include_recording_checkbox);
+ mIncludingUserRecordingCheckBox.setOnClickListener(this);
+
+ mSendButton = (Button) view.findViewById(R.id.research_feedback_send_button);
+ mSendButton.setOnClickListener(this);
+ mCancelButton = (Button) view.findViewById(R.id.research_feedback_cancel_button);
+ mCancelButton.setOnClickListener(this);
- final Button sendButton = (Button) view.findViewById(
- R.id.research_feedback_send_button);
- sendButton.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- final Editable editable = mEditText.getText();
- final String feedbackContents = editable.toString();
- final boolean isIncludingHistory = mIncludingHistoryCheckBox.isChecked();
- final boolean isIncludingAccountName = mIncludingAccountNameCheckBox.isChecked();
- ResearchLogger.getInstance().sendFeedback(feedbackContents, isIncludingHistory,
- isIncludingAccountName);
- final Activity activity = FeedbackFragment.this.getActivity();
- activity.finish();
- ResearchLogger.getInstance().onLeavingSendFeedbackDialog();
+ if (savedInstanceState != null) {
+ Log.d(TAG, "restoring from savedInstanceState");
+ restoreState(savedInstanceState);
+ } else {
+ final Bundle bundle = getActivity().getIntent().getExtras();
+ if (bundle != null) {
+ Log.d(TAG, "restoring from getArguments()");
+ restoreState(bundle);
}
- });
-
- final Button cancelButton = (Button) view.findViewById(
- R.id.research_feedback_cancel_button);
- cancelButton.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- final Activity activity = FeedbackFragment.this.getActivity();
- activity.finish();
- ResearchLogger.getInstance().onLeavingSendFeedbackDialog();
+ }
+ return view;
+ }
+
+ @Override
+ public void onClick(final View view) {
+ final ResearchLogger researchLogger = ResearchLogger.getInstance();
+ if (view == mIncludingUserRecordingCheckBox) {
+ if (hasUserRecording()) {
+ // Remove the recording
+ setHasUserRecording(false);
+ } else {
+ final Bundle bundle = new Bundle();
+ onSaveInstanceState(bundle);
+
+ // Let the user make a recording
+ getActivity().finish();
+
+ researchLogger.setFeedbackDialogBundle(bundle);
+ researchLogger.onLeavingSendFeedbackDialog();
+ researchLogger.startRecording();
}
- });
+ } else if (view == mSendButton) {
+ final Editable editable = mEditText.getText();
+ final String feedbackContents = editable.toString();
+ final boolean isIncludingAccountName = isIncludingAccountName();
+ researchLogger.sendFeedback(feedbackContents,
+ false /* isIncludingHistory */, isIncludingAccountName, hasUserRecording());
+ getActivity().finish();
+ researchLogger.setFeedbackDialogBundle(null);
+ researchLogger.onLeavingSendFeedbackDialog();
+ } else if (view == mCancelButton) {
+ Log.d(TAG, "Finishing");
+ getActivity().finish();
+ researchLogger.setFeedbackDialogBundle(null);
+ researchLogger.onLeavingSendFeedbackDialog();
+ } else {
+ Log.e(TAG, "Unknown view passed to FeedbackFragment.onClick()");
+ }
+ }
- return view;
+ @Override
+ public void onSaveInstanceState(final Bundle bundle) {
+ final String savedFeedbackString = mEditText.getText().toString();
+
+ bundle.putString(KEY_FEEDBACK_STRING, savedFeedbackString);
+ bundle.putBoolean(KEY_INCLUDE_ACCOUNT_NAME, isIncludingAccountName());
+ bundle.putBoolean(KEY_HAS_USER_RECORDING, hasUserRecording());
+ }
+
+ public void restoreState(final Bundle bundle) {
+ mEditText.setText(bundle.getString(KEY_FEEDBACK_STRING));
+ setIsIncludingAccountName(bundle.getBoolean(KEY_INCLUDE_ACCOUNT_NAME));
+ setHasUserRecording(bundle.getBoolean(KEY_HAS_USER_RECORDING));
+ }
+
+ private boolean hasUserRecording() {
+ return mIncludingUserRecordingCheckBox.isChecked();
+ }
+
+ private void setHasUserRecording(final boolean hasRecording) {
+ mIncludingUserRecordingCheckBox.setChecked(hasRecording);
+ }
+
+ private boolean isIncludingAccountName() {
+ return mIncludingAccountNameCheckBox.isChecked();
+ }
+
+ private void setIsIncludingAccountName(final boolean isIncludingAccountName) {
+ mIncludingAccountNameCheckBox.setChecked(isIncludingAccountName);
}
}
diff --git a/java/src/com/android/inputmethod/research/LogStatement.java b/java/src/com/android/inputmethod/research/LogStatement.java
new file mode 100644
index 000000000..090c58e27
--- /dev/null
+++ b/java/src/com/android/inputmethod/research/LogStatement.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.research;
+
+/**
+ * A template for typed information stored in the logs.
+ *
+ * A LogStatement contains a name, keys, and flags about whether the {@code Object[] values}
+ * associated with the {@code String[] keys} are likely to reveal information about the user. The
+ * actual values are stored separately.
+ */
+class LogStatement {
+ // Constants for particular statements
+ public static final String TYPE_POINTER_TRACKER_CALL_LISTENER_ON_CODE_INPUT =
+ "PointerTrackerCallListenerOnCodeInput";
+ public static final String KEY_CODE = "code";
+ public static final String VALUE_RESEARCH = "research";
+ public static final String TYPE_LATIN_KEYBOARD_VIEW_ON_LONG_PRESS =
+ "LatinKeyboardViewOnLongPress";
+ public static final String ACTION = "action";
+ public static final String VALUE_DOWN = "DOWN";
+ public static final String TYPE_LATIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENTS =
+ "LatinKeyboardViewProcessMotionEvents";
+ public static final String KEY_LOGGING_RELATED = "loggingRelated";
+
+ // Name specifying the LogStatement type.
+ private final String mType;
+
+ // mIsPotentiallyPrivate indicates that event contains potentially private information. If
+ // the word that this event is a part of is determined to be privacy-sensitive, then this
+ // event should not be included in the output log. The system waits to output until the
+ // containing word is known.
+ private final boolean mIsPotentiallyPrivate;
+
+ // mIsPotentiallyRevealing indicates that this statement may disclose details about other
+ // words typed in other LogUnits. This can happen if the user is not inserting spaces, and
+ // data from Suggestions and/or Composing text reveals the entire "megaword". For example,
+ // say the user is typing "for the win", and the system wants to record the bigram "the
+ // win". If the user types "forthe", omitting the space, the system will give "for the" as
+ // a suggestion. If the user accepts the autocorrection, the suggestion for "for the" is
+ // included in the log for the word "the", disclosing that the previous word had been "for".
+ // For now, we simply do not include this data when logging part of a "megaword".
+ private final boolean mIsPotentiallyRevealing;
+
+ // mKeys stores the names that are the attributes in the output json objects
+ private final String[] mKeys;
+ private static final String[] NULL_KEYS = new String[0];
+
+ LogStatement(final String name, final boolean isPotentiallyPrivate,
+ final boolean isPotentiallyRevealing, final String... keys) {
+ mType = name;
+ mIsPotentiallyPrivate = isPotentiallyPrivate;
+ mIsPotentiallyRevealing = isPotentiallyRevealing;
+ mKeys = (keys == null) ? NULL_KEYS : keys;
+ }
+
+ public String getType() {
+ return mType;
+ }
+
+ public boolean isPotentiallyPrivate() {
+ return mIsPotentiallyPrivate;
+ }
+
+ public boolean isPotentiallyRevealing() {
+ return mIsPotentiallyRevealing;
+ }
+
+ public String[] getKeys() {
+ return mKeys;
+ }
+
+ /**
+ * Utility function to test whether a key-value pair exists in a LogStatement.
+ *
+ * A LogStatement is really just a template -- it does not contain the values, only the
+ * keys. So the values must be passed in as an argument.
+ *
+ * @param queryKey the String that is tested by {@code String.equals()} to the keys in the
+ * LogStatement
+ * @param queryValue an Object that must be {@code Object.equals()} to the key's corresponding
+ * value in the {@code values} array
+ * @param values the values corresponding to mKeys
+ *
+ * @returns {@true} if {@code queryKey} exists in the keys for this LogStatement, and {@code
+ * queryValue} matches the corresponding value in {@code values}
+ *
+ * @throws IllegalArgumentException if {@code values.length} is not equal to keys().length()
+ */
+ public boolean containsKeyValuePair(final String queryKey, final Object queryValue,
+ final Object[] values) {
+ if (mKeys.length != values.length) {
+ throw new IllegalArgumentException("Mismatched number of keys and values.");
+ }
+ final int length = mKeys.length;
+ for (int i = 0; i < length; i++) {
+ if (mKeys[i].equals(queryKey) && values[i].equals(queryValue)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Utility function to set a value in a LogStatement.
+ *
+ * A LogStatement is really just a template -- it does not contain the values, only the
+ * keys. So the values must be passed in as an argument.
+ *
+ * @param queryKey the String that is tested by {@code String.equals()} to the keys in the
+ * LogStatement
+ * @param values the array of values corresponding to mKeys
+ * @param newValue the replacement value to go into the {@code values} array
+ *
+ * @returns {@true} if the key exists and the value was successfully set, {@false} otherwise
+ *
+ * @throws IllegalArgumentException if {@code values.length} is not equal to keys().length()
+ */
+ public boolean setValue(final String queryKey, final Object[] values, final Object newValue) {
+ if (mKeys.length != values.length) {
+ throw new IllegalArgumentException("Mismatched number of keys and values.");
+ }
+ final int length = mKeys.length;
+ for (int i = 0; i < length; i++) {
+ if (mKeys[i].equals(queryKey)) {
+ values[i] = newValue;
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/java/src/com/android/inputmethod/research/LogUnit.java b/java/src/com/android/inputmethod/research/LogUnit.java
index 638b7d9d4..608fab3f1 100644
--- a/java/src/com/android/inputmethod/research/LogUnit.java
+++ b/java/src/com/android/inputmethod/research/LogUnit.java
@@ -26,15 +26,12 @@ import android.view.inputmethod.CompletionInfo;
import com.android.inputmethod.keyboard.Key;
import com.android.inputmethod.latin.SuggestedWords;
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
-import com.android.inputmethod.latin.Utils;
import com.android.inputmethod.latin.define.ProductionFlag;
-import com.android.inputmethod.research.ResearchLogger.LogStatement;
import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;
-import java.util.Map;
/**
* A group of log statements related to each other.
@@ -53,6 +50,7 @@ import java.util.Map;
/* package */ class LogUnit {
private static final String TAG = LogUnit.class.getSimpleName();
private static final boolean DEBUG = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG;
+
private final ArrayList<LogStatement> mLogStatementList;
private final ArrayList<Object[]> mValuesList;
// Assume that mTimeList is sorted in increasing order. Do not insert null values into
@@ -142,10 +140,10 @@ import java.util.Map;
JsonWriter jsonWriter = null;
for (int i = 0; i < size; i++) {
final LogStatement logStatement = mLogStatementList.get(i);
- if (!canIncludePrivateData && logStatement.mIsPotentiallyPrivate) {
+ if (!canIncludePrivateData && logStatement.isPotentiallyPrivate()) {
continue;
}
- if (mIsPartOfMegaword && logStatement.mIsPotentiallyRevealing) {
+ if (mIsPartOfMegaword && logStatement.isPotentiallyRevealing()) {
continue;
}
// Only retrieve the jsonWriter if we need to. If we don't get this far, then
@@ -228,16 +226,16 @@ import java.util.Map;
private boolean outputLogStatementToLocked(final JsonWriter jsonWriter,
final LogStatement logStatement, final Object[] values, final Long time) {
if (DEBUG) {
- if (logStatement.mKeys.length != values.length) {
- Log.d(TAG, "Key and Value list sizes do not match. " + logStatement.mName);
+ if (logStatement.getKeys().length != values.length) {
+ Log.d(TAG, "Key and Value list sizes do not match. " + logStatement.getType());
}
}
try {
jsonWriter.beginObject();
jsonWriter.name(CURRENT_TIME_KEY).value(System.currentTimeMillis());
jsonWriter.name(UPTIME_KEY).value(time);
- jsonWriter.name(EVENT_TYPE_KEY).value(logStatement.mName);
- final String[] keys = logStatement.mKeys;
+ jsonWriter.name(EVENT_TYPE_KEY).value(logStatement.getType());
+ final String[] keys = logStatement.getKeys();
final int length = values.length;
for (int i = 0; i < length; i++) {
jsonWriter.name(keys[i]);
@@ -261,8 +259,8 @@ import java.util.Map;
} else if (value == null) {
jsonWriter.nullValue();
} else {
- Log.w(TAG, "Unrecognized type to be logged: " +
- (value == null ? "<null>" : value.getClass().getName()));
+ Log.w(TAG, "Unrecognized type to be logged: "
+ + (value == null ? "<null>" : value.getClass().getName()));
jsonWriter.nullValue();
}
}
@@ -422,4 +420,123 @@ import java.util.Map;
}
return false;
}
+
+ /**
+ * Remove data associated with selecting the Research button.
+ *
+ * A LogUnit will capture all user interactions with the IME, including the "meta-interactions"
+ * of using the Research button to control the logging (e.g. by starting and stopping recording
+ * of a test case). Because meta-interactions should not be part of the normal log, calling
+ * this method will set a field in the LogStatements of the motion events to indiciate that
+ * they should be disregarded.
+ *
+ * This implementation assumes that the data recorded by the meta-interaction takes the
+ * form of all events following the first MotionEvent.ACTION_DOWN before the first long-press
+ * before the last onCodeEvent containing a code matching {@code LogStatement.VALUE_RESEARCH}.
+ *
+ * @returns true if data was removed
+ */
+ public boolean removeResearchButtonInvocation() {
+ // This method is designed to be idempotent.
+
+ // First, find last invocation of "research" key
+ final int indexOfLastResearchKey = findLastIndexContainingKeyValue(
+ LogStatement.TYPE_POINTER_TRACKER_CALL_LISTENER_ON_CODE_INPUT,
+ LogStatement.KEY_CODE, LogStatement.VALUE_RESEARCH);
+ if (indexOfLastResearchKey < 0) {
+ // Could not find invocation of "research" key. Leave log as is.
+ if (DEBUG) {
+ Log.d(TAG, "Could not find research key");
+ }
+ return false;
+ }
+
+ // Look for the long press that started the invocation of the research key code input.
+ final int indexOfLastLongPressBeforeResearchKey =
+ findLastIndexBefore(LogStatement.TYPE_LATIN_KEYBOARD_VIEW_ON_LONG_PRESS,
+ indexOfLastResearchKey);
+
+ // Look for DOWN event preceding the long press
+ final int indexOfLastDownEventBeforeLongPress =
+ findLastIndexContainingKeyValueBefore(
+ LogStatement.TYPE_LATIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENTS,
+ LogStatement.ACTION, LogStatement.VALUE_DOWN,
+ indexOfLastLongPressBeforeResearchKey);
+
+ // Flag all LatinKeyboardViewProcessMotionEvents from the DOWN event to the research key as
+ // logging-related
+ final int startingIndex = indexOfLastDownEventBeforeLongPress == -1 ? 0
+ : indexOfLastDownEventBeforeLongPress;
+ for (int index = startingIndex; index < indexOfLastResearchKey; index++) {
+ final LogStatement logStatement = mLogStatementList.get(index);
+ final String type = logStatement.getType();
+ final Object[] values = mValuesList.get(index);
+ if (type.equals(LogStatement.TYPE_LATIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENTS)) {
+ logStatement.setValue(LogStatement.KEY_LOGGING_RELATED, values, true);
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Find the index of the last LogStatement before {@code startingIndex} of type {@code type}.
+ *
+ * @param queryType a String that must be {@code String.equals()} to the LogStatement type
+ * @param startingIndex the index to start the backward search from. Must be less than the
+ * length of mLogStatementList, or an IndexOutOfBoundsException is thrown. Can be negative,
+ * in which case -1 is returned.
+ *
+ * @return The index of the last LogStatement, -1 if none exists.
+ */
+ private int findLastIndexBefore(final String queryType, final int startingIndex) {
+ return findLastIndexContainingKeyValueBefore(queryType, null, null, startingIndex);
+ }
+
+ /**
+ * Find the index of the last LogStatement before {@code startingIndex} of type {@code type}
+ * containing the given key-value pair.
+ *
+ * @param queryType a String that must be {@code String.equals()} to the LogStatement type
+ * @param queryKey a String that must be {@code String.equals()} to a key in the LogStatement
+ * @param queryValue an Object that must be {@code String.equals()} to the key's corresponding
+ * value
+ *
+ * @return The index of the last LogStatement, -1 if none exists.
+ */
+ private int findLastIndexContainingKeyValue(final String queryType, final String queryKey,
+ final Object queryValue) {
+ return findLastIndexContainingKeyValueBefore(queryType, queryKey, queryValue,
+ mLogStatementList.size() - 1);
+ }
+
+ /**
+ * Find the index of the last LogStatement before {@code startingIndex} of type {@code type}
+ * containing the given key-value pair.
+ *
+ * @param queryType a String that must be {@code String.equals()} to the LogStatement type
+ * @param queryKey a String that must be {@code String.equals()} to a key in the LogStatement
+ * @param queryValue an Object that must be {@code String.equals()} to the key's corresponding
+ * value
+ * @param startingIndex the index to start the backward search from. Must be less than the
+ * length of mLogStatementList, or an IndexOutOfBoundsException is thrown. Can be negative,
+ * in which case -1 is returned.
+ *
+ * @return The index of the last LogStatement, -1 if none exists.
+ */
+ private int findLastIndexContainingKeyValueBefore(final String queryType, final String queryKey,
+ final Object queryValue, final int startingIndex) {
+ if (startingIndex < 0) {
+ return -1;
+ }
+ for (int index = startingIndex; index >= 0; index--) {
+ final LogStatement logStatement = mLogStatementList.get(index);
+ final String type = logStatement.getType();
+ if (type.equals(queryType) && (queryKey == null
+ || logStatement.containsKeyValuePair(queryKey, queryValue,
+ mValuesList.get(index)))) {
+ return index;
+ }
+ }
+ return -1;
+ }
}
diff --git a/java/src/com/android/inputmethod/research/MotionEventReader.java b/java/src/com/android/inputmethod/research/MotionEventReader.java
new file mode 100644
index 000000000..36e75be1c
--- /dev/null
+++ b/java/src/com/android/inputmethod/research/MotionEventReader.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.inputmethod.research;
+
+import android.util.JsonReader;
+import android.util.Log;
+import android.view.MotionEvent;
+
+import com.android.inputmethod.latin.define.ProductionFlag;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+
+public class MotionEventReader {
+ private static final String TAG = MotionEventReader.class.getSimpleName();
+ private static final boolean DEBUG = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG;
+
+ public ReplayData readMotionEventData(final File file) {
+ final ReplayData replayData = new ReplayData();
+ try {
+ // Read file
+ final JsonReader jsonReader = new JsonReader(new BufferedReader(new InputStreamReader(
+ new FileInputStream(file))));
+ jsonReader.beginArray();
+ while (jsonReader.hasNext()) {
+ readLogStatement(jsonReader, replayData);
+ }
+ jsonReader.endArray();
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ return replayData;
+ }
+
+ static class ReplayData {
+ final ArrayList<Integer> mActions = new ArrayList<Integer>();
+ final ArrayList<Integer> mXCoords = new ArrayList<Integer>();
+ final ArrayList<Integer> mYCoords = new ArrayList<Integer>();
+ final ArrayList<Long> mTimes = new ArrayList<Long>();
+ }
+
+ private void readLogStatement(final JsonReader jsonReader, final ReplayData replayData)
+ throws IOException {
+ String logStatementType = null;
+ Integer actionType = null;
+ Integer x = null;
+ Integer y = null;
+ Long time = null;
+ boolean loggingRelated = false;
+
+ jsonReader.beginObject();
+ while (jsonReader.hasNext()) {
+ final String key = jsonReader.nextName();
+ if (key.equals("_ty")) {
+ logStatementType = jsonReader.nextString();
+ } else if (key.equals("_ut")) {
+ time = jsonReader.nextLong();
+ } else if (key.equals("x")) {
+ x = jsonReader.nextInt();
+ } else if (key.equals("y")) {
+ y = jsonReader.nextInt();
+ } else if (key.equals("action")) {
+ final String s = jsonReader.nextString();
+ if (s.equals("UP")) {
+ actionType = MotionEvent.ACTION_UP;
+ } else if (s.equals("DOWN")) {
+ actionType = MotionEvent.ACTION_DOWN;
+ } else if (s.equals("MOVE")) {
+ actionType = MotionEvent.ACTION_MOVE;
+ }
+ } else if (key.equals("loggingRelated")) {
+ loggingRelated = jsonReader.nextBoolean();
+ } else {
+ if (DEBUG) {
+ Log.w(TAG, "Unknown JSON key in LogStatement: " + key);
+ }
+ jsonReader.skipValue();
+ }
+ }
+ jsonReader.endObject();
+
+ if (logStatementType != null && time != null && x != null && y != null && actionType != null
+ && logStatementType.equals("MainKeyboardViewProcessMotionEvent")
+ && !loggingRelated) {
+ replayData.mActions.add(actionType);
+ replayData.mXCoords.add(x);
+ replayData.mYCoords.add(y);
+ replayData.mTimes.add(time);
+ }
+ }
+
+}
diff --git a/java/src/com/android/inputmethod/research/Replayer.java b/java/src/com/android/inputmethod/research/Replayer.java
new file mode 100644
index 000000000..4cc2a5814
--- /dev/null
+++ b/java/src/com/android/inputmethod/research/Replayer.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.inputmethod.research;
+
+import android.os.Handler;
+import android.os.Message;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.MotionEvent;
+
+import com.android.inputmethod.keyboard.KeyboardSwitcher;
+import com.android.inputmethod.keyboard.MainKeyboardView;
+import com.android.inputmethod.latin.define.ProductionFlag;
+import com.android.inputmethod.research.MotionEventReader.ReplayData;
+
+/**
+ * Replays a sequence of motion events in realtime on the screen.
+ *
+ * Useful for user inspection of logged data.
+ */
+public class Replayer {
+ private static final String TAG = Replayer.class.getSimpleName();
+ private static final boolean DEBUG = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG;
+ private static final long START_TIME_DELAY_MS = 500;
+
+ private boolean mIsReplaying = false;
+ private KeyboardSwitcher mKeyboardSwitcher;
+
+ public void setKeyboardSwitcher(final KeyboardSwitcher keyboardSwitcher) {
+ mKeyboardSwitcher = keyboardSwitcher;
+ }
+
+ private static final int MSG_MOTION_EVENT = 0;
+ private static final int MSG_DONE = 1;
+ private static final int COMPLETION_TIME_MS = 500;
+
+ // TODO: Support historical events and multi-touch.
+ public void replay(final ReplayData replayData) {
+ if (mIsReplaying) {
+ return;
+ }
+
+ mIsReplaying = true;
+ final int numActions = replayData.mActions.size();
+ if (DEBUG) {
+ Log.d(TAG, "replaying " + numActions + " actions");
+ }
+ if (numActions == 0) {
+ mIsReplaying = false;
+ return;
+ }
+ final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
+
+ // The reference time relative to the times stored in events.
+ final long origStartTime = replayData.mTimes.get(0);
+ // The reference time relative to which events are replayed in the present.
+ final long currentStartTime = SystemClock.uptimeMillis() + START_TIME_DELAY_MS;
+ // The adjustment needed to translate times from the original recorded time to the current
+ // time.
+ final long timeAdjustment = currentStartTime - origStartTime;
+ final Handler handler = new Handler() {
+ // Track the time of the most recent DOWN event, to be passed as a parameter when
+ // constructing a MotionEvent. It's initialized here to the origStartTime, but this is
+ // only a precaution. The value should be overwritten by the first ACTION_DOWN event
+ // before the first use of the variable. Note that this may cause the first few events
+ // to have incorrect {@code downTime}s.
+ private long mOrigDownTime = origStartTime;
+
+ @Override
+ public void handleMessage(final Message msg) {
+ switch (msg.what) {
+ case MSG_MOTION_EVENT:
+ final int index = msg.arg1;
+ final int action = replayData.mActions.get(index);
+ final int x = replayData.mXCoords.get(index);
+ final int y = replayData.mYCoords.get(index);
+ final long origTime = replayData.mTimes.get(index);
+ if (action == MotionEvent.ACTION_DOWN) {
+ mOrigDownTime = origTime;
+ }
+
+ final MotionEvent me = MotionEvent.obtain(mOrigDownTime + timeAdjustment,
+ origTime + timeAdjustment, action, x, y, 0);
+ mainKeyboardView.processMotionEvent(me);
+ me.recycle();
+ break;
+ case MSG_DONE:
+ mIsReplaying = false;
+ break;
+ }
+ }
+ };
+
+ for (int i = 0; i < numActions; i++) {
+ final Message msg = Message.obtain(handler, MSG_MOTION_EVENT, i, 0);
+ final long msgTime = replayData.mTimes.get(i) + timeAdjustment;
+ handler.sendMessageAtTime(msg, msgTime);
+ if (DEBUG) {
+ Log.d(TAG, "queuing event at " + msgTime);
+ }
+ }
+ final long presentDoneTime = replayData.mTimes.get(numActions - 1) + timeAdjustment
+ + COMPLETION_TIME_MS;
+ handler.sendMessageAtTime(Message.obtain(handler, MSG_DONE), presentDoneTime);
+ }
+}
diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java
index dbf2d2982..c4d53e10a 100644
--- a/java/src/com/android/inputmethod/research/ResearchLogger.java
+++ b/java/src/com/android/inputmethod/research/ResearchLogger.java
@@ -39,6 +39,8 @@ 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;
import android.os.IBinder;
import android.os.SystemClock;
import android.preference.PreferenceManager;
@@ -57,6 +59,7 @@ import android.widget.Toast;
import com.android.inputmethod.keyboard.Key;
import com.android.inputmethod.keyboard.Keyboard;
import com.android.inputmethod.keyboard.KeyboardId;
+import com.android.inputmethod.keyboard.KeyboardSwitcher;
import com.android.inputmethod.keyboard.KeyboardView;
import com.android.inputmethod.keyboard.MainKeyboardView;
import com.android.inputmethod.latin.Constants;
@@ -69,8 +72,17 @@ import com.android.inputmethod.latin.RichInputConnection.Range;
import com.android.inputmethod.latin.Suggest;
import com.android.inputmethod.latin.SuggestedWords;
import com.android.inputmethod.latin.define.ProductionFlag;
+import com.android.inputmethod.research.MotionEventReader.ReplayData;
+import java.io.BufferedReader;
import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.MappedByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.charset.Charset;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
@@ -87,8 +99,18 @@ import java.util.UUID;
* This functionality is off by default. See {@link ProductionFlag#IS_EXPERIMENTAL}.
*/
public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChangeListener {
+ // TODO: This class has grown quite large and combines several concerns that should be
+ // separated. The following refactorings will be applied as soon as possible after adding
+ // support for replaying historical events, fixing some replay bugs, adding some ui constraints
+ // on the feedback dialog, and adding the survey dialog.
+ // TODO: Refactor. Move splash screen code into separate class.
+ // TODO: Refactor. Move feedback screen code into separate class.
+ // TODO: Refactor. Move logging invocations into their own class.
+ // TODO: Refactor. Move currentLogUnit management into separate class.
private static final String TAG = ResearchLogger.class.getSimpleName();
private static final boolean DEBUG = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG;
+ private static final boolean DEBUG_REPLAY_AFTER_FEEDBACK = false
+ && ProductionFlag.IS_EXPERIMENTAL_DEBUG;
// Whether the TextView contents are logged at the end of the session. true will disclose
// private info.
private static final boolean LOG_FULL_TEXTVIEW_CONTENTS = false
@@ -98,8 +120,10 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
private static final int OUTPUT_FORMAT_VERSION = 5;
private static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode";
private static final String PREF_RESEARCH_HAS_SEEN_SPLASH = "pref_research_has_seen_splash";
- /* package */ static final String FILENAME_PREFIX = "researchLog";
- private static final String FILENAME_SUFFIX = ".txt";
+ /* package */ static final String LOG_FILENAME_PREFIX = "researchLog";
+ private static final String LOG_FILENAME_SUFFIX = ".txt";
+ /* package */ static final String USER_RECORDING_FILENAME_PREFIX = "recording";
+ private static final String USER_RECORDING_FILENAME_SUFFIX = ".txt";
private static final SimpleDateFormat TIMESTAMP_DATEFORMAT =
new SimpleDateFormat("yyyyMMddHHmmssS", Locale.US);
// Whether to show an indicator on the screen that logging is on. Currently a very small red
@@ -129,9 +153,15 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
// the system to do so.
// LogUnits are queued in the LogBuffers and published to the ResearchLogs when words are
// complete.
- /* package */ ResearchLog mFeedbackLog;
/* package */ MainLogBuffer mMainLogBuffer;
+ // TODO: Remove the feedback log. The feedback log continuously captured user data in case the
+ // user wanted to submit it. We now use the mUserRecordingLogBuffer to allow the user to
+ // explicitly reproduce a problem.
+ /* package */ ResearchLog mFeedbackLog;
/* package */ LogBuffer mFeedbackLogBuffer;
+ /* package */ ResearchLog mUserRecordingLog;
+ /* package */ LogBuffer mUserRecordingLogBuffer;
+ private File mUserRecordingFile = null;
private boolean mIsPasswordView = false;
private boolean mIsLoggingSuspended = false;
@@ -144,7 +174,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
/* package for test */ static final String WORD_REPLACEMENT_STRING = "\uE001";
private static final String PREF_LAST_CLEANUP_TIME = "pref_last_cleanup_time";
private static final long DURATION_BETWEEN_DIR_CLEANUP_IN_MS = DateUtils.DAY_IN_MILLIS;
- private static final long MAX_LOGFILE_AGE_IN_MS = DateUtils.DAY_IN_MILLIS;
+ private static final long MAX_LOGFILE_AGE_IN_MS = 4 * DateUtils.DAY_IN_MILLIS;
protected static final int SUSPEND_DURATION_IN_MINUTES = 1;
// set when LatinIME should ignore an onUpdateSelection() callback that
// arises from operations in this class
@@ -153,10 +183,14 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
// used to check whether words are not unique
private Suggest mSuggest;
private MainKeyboardView mMainKeyboardView;
+ // TODO: Check whether a superclass can be used instead of LatinIME.
private LatinIME mLatinIME;
private final Statistics mStatistics;
+ private final MotionEventReader mMotionEventReader = new MotionEventReader();
+ private final Replayer mReplayer = new Replayer();
private Intent mUploadIntent;
+ private Intent mUploadNowIntent;
private LogUnit mCurrentLogUnit = new LogUnit();
@@ -165,6 +199,20 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
// thereby leaking private data, we store the time of the down event that started the second
// gesture, and when committing the earlier word, split the LogUnit.
private long mSavedDownEventTime;
+ private Bundle mFeedbackDialogBundle = null;
+ private boolean mInFeedbackDialog = false;
+ // The feedback dialog causes stop() to be called for the keyboard connected to the original
+ // window. This is because the feedback dialog must present its own EditText box that displays
+ // a keyboard. stop() normally causes mFeedbackLogBuffer, which contains the user's data, to be
+ // cleared, and causes mFeedbackLog, which is ready to collect information in case the user
+ // wants to upload, to be closed. This is good because we don't need to log information about
+ // what the user is typing in the feedback dialog, but bad because this data must be uploaded.
+ // Here we save the LogBuffer and Log so the feedback dialog can later access their data.
+ private LogBuffer mSavedFeedbackLogBuffer;
+ private ResearchLog mSavedFeedbackLog;
+ private Handler mUserRecordingTimeoutHandler;
+ private static final long USER_RECORDING_TIMEOUT_MS = 30L * DateUtils.SECOND_IN_MILLIS;
+
private ResearchLogger() {
mStatistics = Statistics.getInstance();
}
@@ -173,7 +221,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
return sInstance;
}
- public void init(final LatinIME latinIME) {
+ public void init(final LatinIME latinIME, final KeyboardSwitcher keyboardSwitcher) {
assert latinIME != null;
if (latinIME == null) {
Log.w(TAG, "IMS is null; logging is off");
@@ -210,6 +258,9 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
mLatinIME = latinIME;
mPrefs = prefs;
mUploadIntent = new Intent(mLatinIME, UploaderService.class);
+ mUploadNowIntent = new Intent(mLatinIME, UploaderService.class);
+ mUploadNowIntent.putExtra(UploaderService.EXTRA_UPLOAD_UNCONDITIONALLY, true);
+ mReplayer.setKeyboardSwitcher(keyboardSwitcher);
if (ProductionFlag.IS_EXPERIMENTAL) {
scheduleUploadingService(mLatinIME);
@@ -237,8 +288,10 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
private void cleanupLoggingDir(final File dir, final long time) {
for (File file : dir.listFiles()) {
- if (file.getName().startsWith(ResearchLogger.FILENAME_PREFIX) &&
- file.lastModified() < time) {
+ final String filename = file.getName();
+ if ((filename.startsWith(ResearchLogger.LOG_FILENAME_PREFIX)
+ || filename.startsWith(ResearchLogger.USER_RECORDING_FILENAME_PREFIX))
+ && file.lastModified() < time) {
file.delete();
}
}
@@ -335,9 +388,9 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
private static int sLogFileCounter = 0;
- private File createLogFile(File filesDir) {
+ private File createLogFile(final File filesDir) {
final StringBuilder sb = new StringBuilder();
- sb.append(FILENAME_PREFIX).append('-');
+ sb.append(LOG_FILENAME_PREFIX).append('-');
sb.append(mUUIDString).append('-');
sb.append(TIMESTAMP_DATEFORMAT.format(new Date())).append('-');
// Sometimes logFiles are created within milliseconds of each other. Append a counter to
@@ -349,7 +402,16 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
sLogFileCounter = 0;
}
sb.append(sLogFileCounter);
- sb.append(FILENAME_SUFFIX);
+ sb.append(LOG_FILENAME_SUFFIX);
+ return new File(filesDir, sb.toString());
+ }
+
+ private File createUserRecordingFile(final File filesDir) {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(USER_RECORDING_FILENAME_PREFIX).append('-');
+ sb.append(mUUIDString).append('-');
+ sb.append(TIMESTAMP_DATEFORMAT.format(new Date()));
+ sb.append(USER_RECORDING_FILENAME_SUFFIX);
return new File(filesDir, sb.toString());
}
@@ -517,51 +579,10 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
presentFeedbackDialog(latinIME);
}
- // TODO: currently unreachable. Remove after being sure no menu is needed.
- /*
- public void presentResearchDialog(final LatinIME latinIME) {
- final CharSequence title = latinIME.getString(R.string.english_ime_research_log);
- final boolean showEnable = mIsLoggingSuspended || !sIsLogging;
- final CharSequence[] items = new CharSequence[] {
- latinIME.getString(R.string.research_feedback_menu_option),
- showEnable ? latinIME.getString(R.string.research_enable_session_logging) :
- latinIME.getString(R.string.research_do_not_log_this_session)
- };
- final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface di, int position) {
- di.dismiss();
- switch (position) {
- case 0:
- presentFeedbackDialog(latinIME);
- break;
- case 1:
- enableOrDisable(showEnable, latinIME);
- break;
- }
- }
-
- };
- final AlertDialog.Builder builder = new AlertDialog.Builder(latinIME)
- .setItems(items, listener)
- .setTitle(title);
- latinIME.showOptionDialog(builder.create());
- }
- */
-
- private boolean mInFeedbackDialog = false;
-
- // The feedback dialog causes stop() to be called for the keyboard connected to the original
- // window. This is because the feedback dialog must present its own EditText box that displays
- // a keyboard. stop() normally causes mFeedbackLogBuffer, which contains the user's data, to be
- // cleared, and causes mFeedbackLog, which is ready to collect information in case the user
- // wants to upload, to be closed. This is good because we don't need to log information about
- // what the user is typing in the feedback dialog, but bad because this data must be uploaded.
- // Here we save the LogBuffer and Log so the feedback dialog can later access their data.
- private LogBuffer mSavedFeedbackLogBuffer;
- private ResearchLog mSavedFeedbackLog;
-
public void presentFeedbackDialog(LatinIME latinIME) {
+ if (isMakingUserRecording()) {
+ saveRecording();
+ }
mInFeedbackDialog = true;
mSavedFeedbackLogBuffer = mFeedbackLogBuffer;
mSavedFeedbackLog = mFeedbackLog;
@@ -569,7 +590,90 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
// Feedback dialog will not close them.
mFeedbackLogBuffer = null;
mFeedbackLog = null;
- latinIME.launchKeyboardedDialogActivity(FeedbackActivity.class);
+
+ Intent intent = new Intent();
+ intent.setClass(mLatinIME, FeedbackActivity.class);
+ if (mFeedbackDialogBundle != null) {
+ Log.d(TAG, "putting extra in feedbackdialogbundle");
+ intent.putExtras(mFeedbackDialogBundle);
+ }
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ latinIME.startActivity(intent);
+ }
+
+ public void setFeedbackDialogBundle(final Bundle bundle) {
+ mFeedbackDialogBundle = bundle;
+ }
+
+ public void startRecording() {
+ final Resources res = mLatinIME.getResources();
+ Toast.makeText(mLatinIME,
+ res.getString(R.string.research_feedback_demonstration_instructions),
+ Toast.LENGTH_LONG).show();
+ startRecordingInternal();
+ }
+
+ private void startRecordingInternal() {
+ commitCurrentLogUnit();
+ if (mUserRecordingLog != null) {
+ mUserRecordingLog.abort();
+ }
+ mUserRecordingFile = createUserRecordingFile(mFilesDir);
+ mUserRecordingLog = new ResearchLog(mUserRecordingFile, mLatinIME);
+ mUserRecordingLogBuffer = new LogBuffer();
+ resetRecordingTimer();
+ }
+
+ private boolean isMakingUserRecording() {
+ return mUserRecordingLog != null;
+ }
+
+ private void resetRecordingTimer() {
+ if (mUserRecordingTimeoutHandler == null) {
+ mUserRecordingTimeoutHandler = new Handler();
+ }
+ clearRecordingTimer();
+ mUserRecordingTimeoutHandler.postDelayed(mRecordingHandlerTimeoutRunnable,
+ USER_RECORDING_TIMEOUT_MS);
+ }
+
+ private void clearRecordingTimer() {
+ mUserRecordingTimeoutHandler.removeCallbacks(mRecordingHandlerTimeoutRunnable);
+ }
+
+ private Runnable mRecordingHandlerTimeoutRunnable = new Runnable() {
+ @Override
+ public void run() {
+ cancelRecording();
+ requestIndicatorRedraw();
+ final Resources res = mLatinIME.getResources();
+ Toast.makeText(mLatinIME, res.getString(R.string.research_feedback_recording_failure),
+ Toast.LENGTH_LONG).show();
+ }
+ };
+
+ private void cancelRecording() {
+ if (mUserRecordingLog != null) {
+ mUserRecordingLog.abort();
+ }
+ mUserRecordingLog = null;
+ mUserRecordingLogBuffer = null;
+ if (mFeedbackDialogBundle != null) {
+ mFeedbackDialogBundle.putBoolean("HasRecording", false);
+ }
+ }
+
+ private void saveRecording() {
+ commitCurrentLogUnit();
+ publishLogBuffer(mUserRecordingLogBuffer, mUserRecordingLog, true);
+ mUserRecordingLog.close(null);
+ mUserRecordingLog = null;
+ mUserRecordingLogBuffer = null;
+
+ if (mFeedbackDialogBundle != null) {
+ mFeedbackDialogBundle.putBoolean(FeedbackFragment.KEY_HAS_USER_RECORDING, true);
+ }
+ clearRecordingTimer();
}
// TODO: currently unreachable. Remove after being sure enable/disable is
@@ -631,52 +735,39 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
return null;
}
- static class LogStatement {
- final String mName;
-
- // mIsPotentiallyPrivate indicates that event contains potentially private information. If
- // the word that this event is a part of is determined to be privacy-sensitive, then this
- // event should not be included in the output log. The system waits to output until the
- // containing word is known.
- final boolean mIsPotentiallyPrivate;
-
- // mIsPotentiallyRevealing indicates that this statement may disclose details about other
- // words typed in other LogUnits. This can happen if the user is not inserting spaces, and
- // data from Suggestions and/or Composing text reveals the entire "megaword". For example,
- // say the user is typing "for the win", and the system wants to record the bigram "the
- // win". If the user types "forthe", omitting the space, the system will give "for the" as
- // a suggestion. If the user accepts the autocorrection, the suggestion for "for the" is
- // included in the log for the word "the", disclosing that the previous word had been "for".
- // For now, we simply do not include this data when logging part of a "megaword".
- final boolean mIsPotentiallyRevealing;
-
- // mKeys stores the names that are the attributes in the output json objects
- final String[] mKeys;
- private static final String[] NULL_KEYS = new String[0];
-
- LogStatement(final String name, final boolean isPotentiallyPrivate,
- final boolean isPotentiallyRevealing, final String... keys) {
- mName = name;
- mIsPotentiallyPrivate = isPotentiallyPrivate;
- mIsPotentiallyRevealing = isPotentiallyRevealing;
- mKeys = (keys == null) ? NULL_KEYS : keys;
- }
- }
-
private static final LogStatement LOGSTATEMENT_FEEDBACK =
- new LogStatement("UserFeedback", false, false, "contents", "accountName");
+ new LogStatement("UserFeedback", false, false, "contents", "accountName", "recording");
public void sendFeedback(final String feedbackContents, final boolean includeHistory,
- final boolean isIncludingAccountName) {
+ final boolean isIncludingAccountName, final boolean isIncludingRecording) {
if (mSavedFeedbackLogBuffer == null) {
return;
}
if (!includeHistory) {
mSavedFeedbackLogBuffer.clear();
}
+ String recording = "";
+ if (isIncludingRecording) {
+ // Try to read recording from recently written json file
+ if (mUserRecordingFile != null) {
+ try {
+ final FileChannel channel =
+ new FileInputStream(mUserRecordingFile).getChannel();
+ final MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0,
+ channel.size());
+ // Android's openFileOutput() creates the file, so we use Android's default
+ // Charset (UTF-8) here to read it.
+ recording = Charset.defaultCharset().decode(buffer).toString();
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
final LogUnit feedbackLogUnit = new LogUnit();
final String accountName = isIncludingAccountName ? getAccountName() : "";
feedbackLogUnit.addLogStatement(LOGSTATEMENT_FEEDBACK, SystemClock.uptimeMillis(),
- feedbackContents, accountName);
+ feedbackContents, accountName, recording);
mFeedbackLogBuffer.shiftIn(feedbackLogUnit);
publishLogBuffer(mFeedbackLogBuffer, mSavedFeedbackLog, true /* isIncludingPrivateData */);
mSavedFeedbackLog.close(new Runnable() {
@@ -685,13 +776,25 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
uploadNow();
}
});
+
+ if (isIncludingRecording && DEBUG_REPLAY_AFTER_FEEDBACK) {
+ final Handler handler = new Handler();
+ handler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ final ReplayData replayData =
+ mMotionEventReader.readMotionEventData(mUserRecordingFile);
+ mReplayer.replay(replayData);
+ }
+ }, 1000);
+ }
}
public void uploadNow() {
if (DEBUG) {
Log.d(TAG, "calling uploadNow()");
}
- mLatinIME.startService(mUploadIntent);
+ mLatinIME.startService(mUploadNowIntent);
}
public void onLeavingSendFeedbackDialog() {
@@ -734,11 +837,11 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
int height) {
// TODO: Reimplement using a keyboard background image specific to the ResearchLogger
// and remove this method.
- // The check for MainKeyboardView ensures that a red border is only placed around
- // the main keyboard, not every keyboard.
+ // The check for MainKeyboardView ensures that the indicator only decorates the main
+ // keyboard, not every keyboard.
if (IS_SHOWING_INDICATOR && isAllowedToLog() && view instanceof MainKeyboardView) {
final int savedColor = paint.getColor();
- paint.setColor(Color.RED);
+ paint.setColor(isMakingUserRecording() ? Color.YELLOW : Color.RED);
final Style savedStyle = paint.getStyle();
paint.setStyle(Style.STROKE);
final float savedStrokeWidth = paint.getStrokeWidth();
@@ -747,10 +850,9 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
canvas.drawLine(0, 0, 0, height, paint);
canvas.drawLine(width, 0, width, height, paint);
} else {
- // Put a tiny red dot on the screen so a knowledgeable user can check whether
- // it is enabled. The dot is actually a zero-width, zero-height rectangle,
- // placed at the lower-right corner of the canvas, painted with a non-zero border
- // width.
+ // Put a tiny dot on the screen so a knowledgeable user can check whether it is
+ // enabled. The dot is actually a zero-width, zero-height rectangle, placed at the
+ // lower-right corner of the canvas, painted with a non-zero border width.
paint.setStrokeWidth(3);
canvas.drawRect(width, height, width, height, paint);
}
@@ -770,7 +872,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
private synchronized void enqueueEvent(final LogUnit logUnit, final LogStatement logStatement,
final Object... values) {
- assert values.length == logStatement.mKeys.length;
+ assert values.length == logStatement.getKeys().length;
if (isAllowedToLog() && logUnit != null) {
final long time = SystemClock.uptimeMillis();
logUnit.addLogStatement(logStatement, time, values);
@@ -801,6 +903,9 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
if (mFeedbackLogBuffer != null) {
mFeedbackLogBuffer.shiftIn(mCurrentLogUnit);
}
+ if (mUserRecordingLogBuffer != null) {
+ mUserRecordingLogBuffer.shiftIn(mCurrentLogUnit);
+ }
mCurrentLogUnit = new LogUnit();
} else {
if (DEBUG) {
@@ -1058,7 +1163,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
*
*/
private static final LogStatement LOGSTATEMENT_MAIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENT =
- new LogStatement("MotionEvent", true, false, "action", "MotionEvent");
+ new LogStatement("MotionEvent", true, false, "action", "MotionEvent", "loggingRelated");
public static void mainKeyboardView_processMotionEvent(final MotionEvent me, final int action,
final long eventTime, final int index, final int id, final int x, final int y) {
if (me != null) {
@@ -1075,12 +1180,16 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
}
final ResearchLogger researchLogger = getInstance();
researchLogger.enqueueEvent(LOGSTATEMENT_MAIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENT,
- actionString, MotionEvent.obtain(me));
+ actionString, MotionEvent.obtain(me), false);
if (action == MotionEvent.ACTION_DOWN) {
// Subtract 1 from eventTime so the down event is included in the later
// LogUnit, not the earlier (the test is for inequality).
researchLogger.setSavedDownEventTime(eventTime - 1);
}
+ // Refresh the timer in case we are capturing user feedback.
+ if (researchLogger.isMakingUserRecording()) {
+ researchLogger.resetRecordingTimer();
+ }
}
}
@@ -1442,13 +1551,21 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
final int code) {
if (key != null) {
String outputText = key.getOutputText();
- getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCODEINPUT,
+ final ResearchLogger researchLogger = getInstance();
+ researchLogger.enqueueEvent(LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCODEINPUT,
Constants.printableCode(scrubDigitFromCodePoint(code)),
outputText == null ? null : scrubDigitsFromString(outputText.toString()),
x, y, ignoreModifierKey, altersCode, key.isEnabled());
+ if (code == Constants.CODE_RESEARCH) {
+ researchLogger.suppressResearchKeyMotionData();
+ }
}
}
+ private void suppressResearchKeyMotionData() {
+ mCurrentLogUnit.removeResearchButtonInvocation();
+ }
+
/**
* Log a call to PointerTracker.callListenerCallListenerOnRelease().
*
diff --git a/java/src/com/android/inputmethod/research/UploaderService.java b/java/src/com/android/inputmethod/research/UploaderService.java
index 5e3cf55e4..89c67fbb2 100644
--- a/java/src/com/android/inputmethod/research/UploaderService.java
+++ b/java/src/com/android/inputmethod/research/UploaderService.java
@@ -51,7 +51,7 @@ public final class UploaderService extends IntentService {
private static final boolean IS_INHIBITING_AUTO_UPLOAD = false
&& ProductionFlag.IS_EXPERIMENTAL_DEBUG; // Force false in production
public static final long RUN_INTERVAL = AlarmManager.INTERVAL_HOUR;
- private static final String EXTRA_UPLOAD_UNCONDITIONALLY = UploaderService.class.getName()
+ public static final String EXTRA_UPLOAD_UNCONDITIONALLY = UploaderService.class.getName()
+ ".extra.UPLOAD_UNCONDITIONALLY";
private static final int BUF_SIZE = 1024 * 8;
protected static final int TIMEOUT_IN_MS = 1000 * 4;
@@ -131,7 +131,7 @@ public final class UploaderService extends IntentService {
final File[] files = mFilesDir.listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
- return pathname.getName().startsWith(ResearchLogger.FILENAME_PREFIX)
+ return pathname.getName().startsWith(ResearchLogger.LOG_FILENAME_PREFIX)
&& !pathname.canWrite();
}
});