diff options
Diffstat (limited to 'java/src/com/android/inputmethod')
104 files changed, 6237 insertions, 4207 deletions
diff --git a/java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java b/java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java index 56f9c2ab2..5af5d044f 100644 --- a/java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java +++ b/java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java @@ -35,6 +35,7 @@ import android.view.inputmethod.EditorInfo; import com.android.inputmethod.keyboard.Key; import com.android.inputmethod.keyboard.Keyboard; import com.android.inputmethod.keyboard.KeyboardView; +import com.android.inputmethod.latin.CollectionUtils; /** * Exposes a virtual view sub-tree for {@link KeyboardView} and generates @@ -55,7 +56,7 @@ public class AccessibilityEntityProvider extends AccessibilityNodeProviderCompat private final AccessibilityUtils mAccessibilityUtils; /** A map of integer IDs to {@link Key}s. */ - private final SparseArray<Key> mVirtualViewIdToKey = new SparseArray<Key>(); + private final SparseArray<Key> mVirtualViewIdToKey = CollectionUtils.newSparseArray(); /** Temporary rect used to calculate in-screen bounds. */ private final Rect mTempBoundsInScreen = new Rect(); diff --git a/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java b/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java index 58d3022c9..1eee1df87 100644 --- a/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java +++ b/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java @@ -37,7 +37,7 @@ import com.android.inputmethod.compat.SettingsSecureCompatUtils; import com.android.inputmethod.latin.InputTypeUtils; import com.android.inputmethod.latin.R; -public class AccessibilityUtils { +public final class AccessibilityUtils { private static final String TAG = AccessibilityUtils.class.getSimpleName(); private static final String CLASS = AccessibilityUtils.class.getClass().getName(); private static final String PACKAGE = AccessibilityUtils.class.getClass().getPackage() diff --git a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java index ed3468afb..77940c086 100644 --- a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java +++ b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java @@ -24,7 +24,6 @@ import android.support.v4.view.accessibility.AccessibilityEventCompat; import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; import android.view.MotionEvent; import android.view.View; -import android.view.ViewConfiguration; import com.android.inputmethod.keyboard.Key; import com.android.inputmethod.keyboard.Keyboard; @@ -44,7 +43,7 @@ public class AccessibleKeyboardViewProxy extends AccessibilityDelegateCompat { /** * Inset in pixels to look for keys when the user's finger exits the - * keyboard area. See {@link ViewConfiguration#getScaledEdgeSlop()}. + * keyboard area. */ private int mEdgeSlop; @@ -62,7 +61,8 @@ public class AccessibleKeyboardViewProxy extends AccessibilityDelegateCompat { private void initInternal(InputMethodService inputMethod) { mInputMethod = inputMethod; - mEdgeSlop = ViewConfiguration.get(inputMethod).getScaledEdgeSlop(); + mEdgeSlop = inputMethod.getResources().getDimensionPixelSize( + R.dimen.accessibility_edge_slop); } /** @@ -127,8 +127,14 @@ public class AccessibleKeyboardViewProxy extends AccessibilityDelegateCompat { public boolean dispatchHoverEvent(MotionEvent event, PointerTracker tracker) { final int x = (int) event.getX(); final int y = (int) event.getY(); - final Key key = tracker.getKeyOn(x, y); final Key previousKey = mLastHoverKey; + final Key key; + + if (pointInView(x, y)) { + key = tracker.getKeyOn(x, y); + } else { + key = null; + } mLastHoverKey = key; @@ -136,7 +142,7 @@ public class AccessibleKeyboardViewProxy extends AccessibilityDelegateCompat { case MotionEvent.ACTION_HOVER_EXIT: // Make sure we're not getting an EXIT event because the user slid // off the keyboard area, then force a key press. - if (pointInView(x, y) && (key != null)) { + if (key != null) { getAccessibilityNodeProvider().simulateKeyPress(key); } //$FALL-THROUGH$ diff --git a/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java b/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java index 9b74070af..5c45448a5 100644 --- a/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java +++ b/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java @@ -25,6 +25,7 @@ import android.view.inputmethod.EditorInfo; import com.android.inputmethod.keyboard.Key; import com.android.inputmethod.keyboard.Keyboard; import com.android.inputmethod.keyboard.KeyboardId; +import com.android.inputmethod.latin.CollectionUtils; import com.android.inputmethod.latin.R; import java.util.HashMap; @@ -38,7 +39,7 @@ public class KeyCodeDescriptionMapper { private static KeyCodeDescriptionMapper sInstance = new KeyCodeDescriptionMapper(); // Map of key labels to spoken description resource IDs - private final HashMap<CharSequence, Integer> mKeyLabelMap; + private final HashMap<CharSequence, Integer> mKeyLabelMap = CollectionUtils.newHashMap(); // Sparse array of spoken description resource IDs indexed by key codes private final SparseIntArray mKeyCodeMap; @@ -52,7 +53,6 @@ public class KeyCodeDescriptionMapper { } private KeyCodeDescriptionMapper() { - mKeyLabelMap = new HashMap<CharSequence, Integer>(); mKeyCodeMap = new SparseIntArray(); } diff --git a/java/src/com/android/inputmethod/compat/CompatUtils.java b/java/src/com/android/inputmethod/compat/CompatUtils.java index ce427e9c9..ffed6ecb1 100644 --- a/java/src/com/android/inputmethod/compat/CompatUtils.java +++ b/java/src/com/android/inputmethod/compat/CompatUtils.java @@ -24,7 +24,7 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; -public class CompatUtils { +public final class CompatUtils { private static final String TAG = CompatUtils.class.getSimpleName(); private static final String EXTRA_INPUT_METHOD_ID = "input_method_id"; // TODO: Can these be constants instead of literal String constants? diff --git a/java/src/com/android/inputmethod/compat/EditorInfoCompatUtils.java b/java/src/com/android/inputmethod/compat/EditorInfoCompatUtils.java index 08c246f8b..210058bec 100644 --- a/java/src/com/android/inputmethod/compat/EditorInfoCompatUtils.java +++ b/java/src/com/android/inputmethod/compat/EditorInfoCompatUtils.java @@ -20,7 +20,7 @@ import android.view.inputmethod.EditorInfo; import java.lang.reflect.Field; -public class EditorInfoCompatUtils { +public final class EditorInfoCompatUtils { // EditorInfo.IME_FLAG_FORCE_ASCII has been introduced since API#16 (JellyBean). private static final Field FIELD_IME_FLAG_FORCE_ASCII = CompatUtils.getField( EditorInfo.class, "IME_FLAG_FORCE_ASCII"); diff --git a/java/src/com/android/inputmethod/compat/InputMethodServiceCompatUtils.java b/java/src/com/android/inputmethod/compat/InputMethodServiceCompatUtils.java index 0befa7a66..8eea31ed2 100644 --- a/java/src/com/android/inputmethod/compat/InputMethodServiceCompatUtils.java +++ b/java/src/com/android/inputmethod/compat/InputMethodServiceCompatUtils.java @@ -20,7 +20,7 @@ import android.inputmethodservice.InputMethodService; import java.lang.reflect.Method; -public class InputMethodServiceCompatUtils { +public final class InputMethodServiceCompatUtils { private static final Method METHOD_enableHardwareAcceleration = CompatUtils.getMethod(InputMethodService.class, "enableHardwareAcceleration"); diff --git a/java/src/com/android/inputmethod/compat/SettingsSecureCompatUtils.java b/java/src/com/android/inputmethod/compat/SettingsSecureCompatUtils.java index 1b79992f0..db5abd0fe 100644 --- a/java/src/com/android/inputmethod/compat/SettingsSecureCompatUtils.java +++ b/java/src/com/android/inputmethod/compat/SettingsSecureCompatUtils.java @@ -18,7 +18,7 @@ package com.android.inputmethod.compat; import java.lang.reflect.Field; -public class SettingsSecureCompatUtils { +public final class SettingsSecureCompatUtils { private static final Field FIELD_ACCESSIBILITY_SPEAK_PASSWORD = CompatUtils.getField( android.provider.Settings.Secure.class, "ACCESSIBILITY_SPEAK_PASSWORD"); diff --git a/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java b/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java index 1183b5fb9..159f43650 100644 --- a/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java +++ b/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java @@ -16,10 +16,6 @@ package com.android.inputmethod.compat; -import com.android.inputmethod.latin.LatinImeLogger; -import com.android.inputmethod.latin.SuggestedWords; -import com.android.inputmethod.latin.SuggestionSpanPickedNotificationReceiver; - import android.content.Context; import android.text.Spannable; import android.text.SpannableString; @@ -27,12 +23,17 @@ import android.text.Spanned; import android.text.TextUtils; import android.util.Log; +import com.android.inputmethod.latin.CollectionUtils; +import com.android.inputmethod.latin.LatinImeLogger; +import com.android.inputmethod.latin.SuggestedWords; +import com.android.inputmethod.latin.SuggestionSpanPickedNotificationReceiver; + import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Locale; -public class SuggestionSpanUtils { +public final class SuggestionSpanUtils { private static final String TAG = SuggestionSpanUtils.class.getSimpleName(); // TODO: Use reflection to get field values public static final String ACTION_SUGGESTION_PICKED = @@ -119,7 +120,7 @@ public class SuggestionSpanUtils { } else { spannable = new SpannableString(pickedWord); } - final ArrayList<String> suggestionsList = new ArrayList<String>(); + final ArrayList<String> suggestionsList = CollectionUtils.newArrayList(); for (int i = 0; i < suggestedWords.size(); ++i) { if (suggestionsList.size() >= OBJ_SUGGESTIONS_MAX_SIZE) { break; diff --git a/java/src/com/android/inputmethod/compat/SuggestionsInfoCompatUtils.java b/java/src/com/android/inputmethod/compat/SuggestionsInfoCompatUtils.java index e5f9db27c..8314212c9 100644 --- a/java/src/com/android/inputmethod/compat/SuggestionsInfoCompatUtils.java +++ b/java/src/com/android/inputmethod/compat/SuggestionsInfoCompatUtils.java @@ -20,7 +20,7 @@ import android.view.textservice.SuggestionsInfo; import java.lang.reflect.Field; -public class SuggestionsInfoCompatUtils { +public final class SuggestionsInfoCompatUtils { private static final Field FIELD_RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS = CompatUtils.getField( SuggestionsInfo.class, "RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS"); private static final Integer OBJ_RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS = (Integer) CompatUtils diff --git a/java/src/com/android/inputmethod/keyboard/Key.java b/java/src/com/android/inputmethod/keyboard/Key.java index 178c9ff05..03c216474 100644 --- a/java/src/com/android/inputmethod/keyboard/Key.java +++ b/java/src/com/android/inputmethod/keyboard/Key.java @@ -31,11 +31,16 @@ import android.text.TextUtils; import android.util.Log; import android.util.Xml; +import com.android.inputmethod.keyboard.internal.KeyDrawParams; import com.android.inputmethod.keyboard.internal.KeySpecParser; -import com.android.inputmethod.keyboard.internal.KeySpecParser.MoreKeySpec; -import com.android.inputmethod.keyboard.internal.KeyStyles.KeyStyle; +import com.android.inputmethod.keyboard.internal.KeyStyle; +import com.android.inputmethod.keyboard.internal.KeyVisualAttributes; import com.android.inputmethod.keyboard.internal.KeyboardIconsSet; +import com.android.inputmethod.keyboard.internal.KeyboardParams; +import com.android.inputmethod.keyboard.internal.KeyboardRow; +import com.android.inputmethod.keyboard.internal.MoreKeySpec; import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.ResourceUtils; import com.android.inputmethod.latin.StringUtils; import org.xmlpull.v1.XmlPullParser; @@ -54,7 +59,6 @@ public class Key { * The key code (unicode or custom code) that this key generates. */ public final int mCode; - public final int mAltCode; /** Label to display */ public final String mLabel; @@ -89,22 +93,11 @@ public class Key { /** Icon to display instead of a label. Icon takes precedence over a label */ private final int mIconId; - /** Icon for disabled state */ - private final int mDisabledIconId; - /** Preview version of the icon, for the preview popup */ - private final int mPreviewIconId; /** Width of the key, not including the gap */ public final int mWidth; /** Height of the key, not including the gap */ public final int mHeight; - /** The horizontal gap around this key */ - public final int mHorizontalGap; - /** The vertical gap below this key */ - public final int mVerticalGap; - /** The visual insets */ - public final int mVisualInsetsLeft; - public final int mVisualInsetsRight; /** X coordinate of the key in the keyboard layout */ public final int mX; /** Y coordinate of the key in the keyboard layout */ @@ -112,8 +105,6 @@ public class Key { /** Hit bounding box of the key */ public final Rect mHitBox = new Rect(); - /** Text to output when pressed. This can be multiple characters, like ".com" */ - public final CharSequence mOutputText; /** More keys */ public final MoreKeySpec[] mMoreKeys; /** More keys column number and flags */ @@ -143,6 +134,34 @@ public class Key { private static final int ACTION_FLAGS_ALT_CODE_WHILE_TYPING = 0x04; private static final int ACTION_FLAGS_ENABLE_LONG_PRESS = 0x08; + public final KeyVisualAttributes mKeyVisualAttributes; + + private final OptionalAttributes mOptionalAttributes; + + private static class OptionalAttributes { + /** Text to output when pressed. This can be multiple characters, like ".com" */ + public final String mOutputText; + public final int mAltCode; + /** Icon for disabled state */ + public final int mDisabledIconId; + /** Preview version of the icon, for the preview popup */ + public final int mPreviewIconId; + /** The visual insets */ + public final int mVisualInsetsLeft; + public final int mVisualInsetsRight; + + public OptionalAttributes(final String outputText, final int altCode, + final int disabledIconId, final int previewIconId, + final int visualInsetsLeft, final int visualInsetsRight) { + mOutputText = outputText; + mAltCode = altCode; + mDisabledIconId = disabledIconId; + mPreviewIconId = previewIconId; + mVisualInsetsLeft = visualInsetsLeft; + mVisualInsetsRight = visualInsetsRight; + } + } + private final int mHashCode; /** The current pressed state of this key */ @@ -153,8 +172,8 @@ public class Key { /** * This constructor is being used only for keys in more keys keyboard. */ - public Key(Keyboard.Params params, MoreKeySpec moreKeySpec, int x, int y, int width, int height, - int labelFlags) { + public Key(final KeyboardParams params, final MoreKeySpec moreKeySpec, final int x, final int y, + final int width, final int height, final int labelFlags) { this(params, moreKeySpec.mLabel, null, moreKeySpec.mIconId, moreKeySpec.mCode, moreKeySpec.mOutputText, x, y, width, height, labelFlags); } @@ -162,13 +181,11 @@ public class Key { /** * This constructor is being used only for key in popup suggestions pane. */ - public Key(Keyboard.Params params, String label, String hintLabel, int iconId, - int code, String outputText, int x, int y, int width, int height, int labelFlags) { + public Key(final KeyboardParams params, final String label, final String hintLabel, + final int iconId, final int code, final String outputText, final int x, final int y, + final int width, final int height, final int labelFlags) { mHeight = height - params.mVerticalGap; - mHorizontalGap = params.mHorizontalGap; - mVerticalGap = params.mVerticalGap; - mVisualInsetsLeft = mVisualInsetsRight = 0; - mWidth = width - mHorizontalGap; + mWidth = width - params.mHorizontalGap; mHintLabel = hintLabel; mLabelFlags = labelFlags; mBackgroundType = BACKGROUND_TYPE_NORMAL; @@ -176,17 +193,20 @@ public class Key { mMoreKeys = null; mMoreKeysColumnAndFlags = 0; mLabel = label; - mOutputText = outputText; + if (outputText == null) { + mOptionalAttributes = null; + } else { + mOptionalAttributes = new OptionalAttributes(outputText, CODE_UNSPECIFIED, + ICON_UNDEFINED, ICON_UNDEFINED, 0, 0); + } mCode = code; mEnabled = (code != CODE_UNSPECIFIED); - mAltCode = CODE_UNSPECIFIED; mIconId = iconId; - mDisabledIconId = ICON_UNDEFINED; - mPreviewIconId = ICON_UNDEFINED; // Horizontal gap is divided equally to both sides of the key. - mX = x + mHorizontalGap / 2; + mX = x + params.mHorizontalGap / 2; mY = y; mHitBox.set(x, y, x + width + 1, y + height); + mKeyVisualAttributes = null; mHashCode = computeHashCode(this); } @@ -201,12 +221,11 @@ public class Key { * @param parser the XML parser containing the attributes for this key * @throws XmlPullParserException */ - public Key(Resources res, Keyboard.Params params, Keyboard.Builder.Row row, - XmlPullParser parser) throws XmlPullParserException { + 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; - mVerticalGap = params.mVerticalGap; - mHeight = keyHeight - mVerticalGap; + mHeight = keyHeight - params.mVerticalGap; final TypedArray keyAttr = res.obtainAttributes(Xml.asAttributeSet(parser), R.styleable.Keyboard_Key); @@ -220,7 +239,6 @@ public class Key { mX = Math.round(keyXPos + horizontalGap / 2); mY = keyYPos; mWidth = Math.round(keyWidth - horizontalGap); - mHorizontalGap = Math.round(horizontalGap); mHitBox.set(Math.round(keyXPos), keyYPos, Math.round(keyXPos + keyWidth) + 1, keyYPos + keyHeight); // Update row to have current x coordinate. @@ -229,15 +247,15 @@ public class Key { mBackgroundType = style.getInt(keyAttr, R.styleable.Keyboard_Key_backgroundType, row.getDefaultBackgroundType()); - mVisualInsetsLeft = Math.round(Keyboard.Builder.getDimensionOrFraction(keyAttr, + final int visualInsetsLeft = Math.round(ResourceUtils.getDimensionOrFraction(keyAttr, R.styleable.Keyboard_Key_visualInsetsLeft, params.mBaseWidth, 0)); - mVisualInsetsRight = Math.round(Keyboard.Builder.getDimensionOrFraction(keyAttr, + final int visualInsetsRight = Math.round(ResourceUtils.getDimensionOrFraction(keyAttr, R.styleable.Keyboard_Key_visualInsetsRight, params.mBaseWidth, 0)); mIconId = KeySpecParser.getIconId(style.getString(keyAttr, R.styleable.Keyboard_Key_keyIcon)); - mDisabledIconId = KeySpecParser.getIconId(style.getString(keyAttr, + final int disabledIconId = KeySpecParser.getIconId(style.getString(keyAttr, R.styleable.Keyboard_Key_keyIconDisabled)); - mPreviewIconId = KeySpecParser.getIconId(style.getString(keyAttr, + final int previewIconId = KeySpecParser.getIconId(style.getString(keyAttr, R.styleable.Keyboard_Key_keyIconPreview)); mLabelFlags = style.getFlag(keyAttr, R.styleable.Keyboard_Key_keyLabelFlags) @@ -331,21 +349,28 @@ public class Key { } else { mCode = KeySpecParser.toUpperCaseOfCodeForLocale(code, needsToUpperCase, locale); } - mOutputText = outputText; - mAltCode = KeySpecParser.toUpperCaseOfCodeForLocale( + final int altCode = KeySpecParser.toUpperCaseOfCodeForLocale( KeySpecParser.parseCode(style.getString(keyAttr, R.styleable.Keyboard_Key_altCode), params.mCodesSet, CODE_UNSPECIFIED), needsToUpperCase, locale); - mHashCode = computeHashCode(this); - + if (outputText == null && altCode == CODE_UNSPECIFIED + && disabledIconId == ICON_UNDEFINED && previewIconId == ICON_UNDEFINED + && visualInsetsLeft == 0 && visualInsetsRight == 0) { + mOptionalAttributes = null; + } else { + mOptionalAttributes = new OptionalAttributes(outputText, altCode, + disabledIconId, previewIconId, + visualInsetsLeft, visualInsetsRight); + } + mKeyVisualAttributes = KeyVisualAttributes.newInstance(keyAttr); keyAttr.recycle(); - + mHashCode = computeHashCode(this); if (hasShiftedLetterHint() && TextUtils.isEmpty(mHintLabel)) { Log.w(TAG, "hasShiftedLetterHint specified without keyHintLabel: " + this); } } - private static boolean needsToUpperCase(int labelFlags, int keyboardElementId) { + private static boolean needsToUpperCase(final int labelFlags, final int keyboardElementId) { if ((labelFlags & LABEL_FLAGS_PRESERVE_CASE) != 0) return false; switch (keyboardElementId) { case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED: @@ -358,7 +383,7 @@ public class Key { } } - private static int computeHashCode(Key key) { + private static int computeHashCode(final Key key) { return Arrays.hashCode(new Object[] { key.mX, key.mY, @@ -370,22 +395,22 @@ public class Key { key.mIconId, key.mBackgroundType, Arrays.hashCode(key.mMoreKeys), - key.mOutputText, + key.getOutputText(), key.mActionFlags, key.mLabelFlags, // Key can be distinguishable without the following members. - // key.mAltCode, - // key.mDisabledIconId, - // key.mPreviewIconId, + // key.mOptionalAttributes.mAltCode, + // key.mOptionalAttributes.mDisabledIconId, + // key.mOptionalAttributes.mPreviewIconId, // key.mHorizontalGap, // key.mVerticalGap, - // key.mVisualInsetLeft, - // key.mVisualInsetRight, + // key.mOptionalAttributes.mVisualInsetLeft, + // key.mOptionalAttributes.mVisualInsetRight, // key.mMaxMoreKeysColumn, }); } - private boolean equals(Key o) { + private boolean equals(final Key o) { if (this == o) return true; return o.mX == mX && o.mY == mY @@ -397,7 +422,7 @@ public class Key { && o.mIconId == mIconId && o.mBackgroundType == mBackgroundType && Arrays.equals(o.mMoreKeys, mMoreKeys) - && TextUtils.equals(o.mOutputText, mOutputText) + && TextUtils.equals(o.getOutputText(), getOutputText()) && o.mActionFlags == mActionFlags && o.mLabelFlags == mLabelFlags; } @@ -408,7 +433,7 @@ public class Key { } @Override - public boolean equals(Object o) { + public boolean equals(final Object o) { return o instanceof Key && equals((Key)o); } @@ -425,7 +450,7 @@ public class Key { KeyboardIconsSet.getIconName(mIconId), backgroundName(mBackgroundType)); } - private static String backgroundName(int backgroundType) { + private static String backgroundName(final int backgroundType) { switch (backgroundType) { case BACKGROUND_TYPE_NORMAL: return "normal"; case BACKGROUND_TYPE_FUNCTIONAL: return "functional"; @@ -436,19 +461,19 @@ public class Key { } } - public void markAsLeftEdge(Keyboard.Params params) { + public void markAsLeftEdge(final KeyboardParams params) { mHitBox.left = params.mHorizontalEdgesPadding; } - public void markAsRightEdge(Keyboard.Params params) { + public void markAsRightEdge(final KeyboardParams params) { mHitBox.right = params.mOccupiedWidth - params.mHorizontalEdgesPadding; } - public void markAsTopEdge(Keyboard.Params params) { + public void markAsTopEdge(final KeyboardParams params) { mHitBox.top = params.mTopPadding; } - public void markAsBottomEdge(Keyboard.Params params) { + public void markAsBottomEdge(final KeyboardParams params) { mHitBox.bottom = params.mOccupiedHeight + params.mBottomPadding; } @@ -456,129 +481,169 @@ public class Key { return this instanceof Spacer; } - public boolean isShift() { + public final boolean isShift() { return mCode == CODE_SHIFT; } - public boolean isModifier() { + public final boolean isModifier() { return mCode == CODE_SHIFT || mCode == CODE_SWITCH_ALPHA_SYMBOL; } - public boolean isRepeatable() { + public final boolean isRepeatable() { return (mActionFlags & ACTION_FLAGS_IS_REPEATABLE) != 0; } - public boolean noKeyPreview() { + public final boolean noKeyPreview() { return (mActionFlags & ACTION_FLAGS_NO_KEY_PREVIEW) != 0; } - public boolean altCodeWhileTyping() { + public final boolean altCodeWhileTyping() { return (mActionFlags & ACTION_FLAGS_ALT_CODE_WHILE_TYPING) != 0; } - public boolean isLongPressEnabled() { + public final boolean isLongPressEnabled() { // We need not start long press timer on the key which has activated shifted letter. return (mActionFlags & ACTION_FLAGS_ENABLE_LONG_PRESS) != 0 && (mLabelFlags & LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED) == 0; } - public Typeface selectTypeface(Typeface defaultTypeface) { + public final Typeface selectTypeface(final KeyDrawParams params) { // TODO: Handle "bold" here too? if ((mLabelFlags & LABEL_FLAGS_FONT_NORMAL) != 0) { return Typeface.DEFAULT; } else if ((mLabelFlags & LABEL_FLAGS_FONT_MONO_SPACE) != 0) { return Typeface.MONOSPACE; } else { - return defaultTypeface; + return params.mTypeface; } } - public int selectTextSize(int letterSize, int largeLetterSize, int labelSize, - int largeLabelSize, int hintLabelSize) { + public final int selectTextSize(final KeyDrawParams params) { switch (mLabelFlags & LABEL_FLAGS_FOLLOW_KEY_TEXT_RATIO_MASK) { case LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO: - return letterSize; + return params.mLetterSize; case LABEL_FLAGS_FOLLOW_KEY_LARGE_LETTER_RATIO: - return largeLetterSize; + return params.mLargeLetterSize; case LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO: - return labelSize; + return params.mLabelSize; case LABEL_FLAGS_FOLLOW_KEY_LARGE_LABEL_RATIO: - return largeLabelSize; + return params.mLargeLabelSize; case LABEL_FLAGS_FOLLOW_KEY_HINT_LABEL_RATIO: - return hintLabelSize; + return params.mHintLabelSize; default: // No follow key ratio flag specified. - return StringUtils.codePointCount(mLabel) == 1 ? letterSize : labelSize; + return StringUtils.codePointCount(mLabel) == 1 ? params.mLetterSize : params.mLabelSize; } } - public boolean isAlignLeft() { + public final int selectTextColor(final KeyDrawParams params) { + return isShiftedLetterActivated() ? params.mTextInactivatedColor : params.mTextColor; + } + + public final int selectHintTextSize(final KeyDrawParams params) { + if (hasHintLabel()) { + return params.mHintLabelSize; + } else if (hasShiftedLetterHint()) { + return params.mShiftedLetterHintSize; + } else { + return params.mHintLetterSize; + } + } + + public final int selectHintTextColor(final KeyDrawParams params) { + if (hasHintLabel()) { + return params.mHintLabelColor; + } else if (hasShiftedLetterHint()) { + return isShiftedLetterActivated() ? params.mShiftedLetterHintActivatedColor + : params.mShiftedLetterHintInactivatedColor; + } else { + return params.mHintLetterColor; + } + } + + public final int selectMoreKeyTextSize(final KeyDrawParams params) { + return hasLabelsInMoreKeys() ? params.mLabelSize : params.mLetterSize; + } + + public final boolean isAlignLeft() { return (mLabelFlags & LABEL_FLAGS_ALIGN_LEFT) != 0; } - public boolean isAlignRight() { + public final boolean isAlignRight() { return (mLabelFlags & LABEL_FLAGS_ALIGN_RIGHT) != 0; } - public boolean isAlignLeftOfCenter() { + public final boolean isAlignLeftOfCenter() { return (mLabelFlags & LABEL_FLAGS_ALIGN_LEFT_OF_CENTER) != 0; } - public boolean hasPopupHint() { + public final boolean hasPopupHint() { return (mLabelFlags & LABEL_FLAGS_HAS_POPUP_HINT) != 0; } - public boolean hasShiftedLetterHint() { + public final boolean hasShiftedLetterHint() { return (mLabelFlags & LABEL_FLAGS_HAS_SHIFTED_LETTER_HINT) != 0; } - public boolean hasHintLabel() { + public final boolean hasHintLabel() { return (mLabelFlags & LABEL_FLAGS_HAS_HINT_LABEL) != 0; } - public boolean hasLabelWithIconLeft() { + public final boolean hasLabelWithIconLeft() { return (mLabelFlags & LABEL_FLAGS_WITH_ICON_LEFT) != 0; } - public boolean hasLabelWithIconRight() { + public final boolean hasLabelWithIconRight() { return (mLabelFlags & LABEL_FLAGS_WITH_ICON_RIGHT) != 0; } - public boolean needsXScale() { + public final boolean needsXScale() { return (mLabelFlags & LABEL_FLAGS_AUTO_X_SCALE) != 0; } - public boolean isShiftedLetterActivated() { + public final boolean isShiftedLetterActivated() { return (mLabelFlags & LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED) != 0; } - public int getMoreKeysColumn() { + public final int getMoreKeysColumn() { return mMoreKeysColumnAndFlags & MORE_KEYS_COLUMN_MASK; } - public boolean isFixedColumnOrderMoreKeys() { + public final boolean isFixedColumnOrderMoreKeys() { return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_FIXED_COLUMN_ORDER) != 0; } - public boolean hasLabelsInMoreKeys() { + public final boolean hasLabelsInMoreKeys() { return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_HAS_LABELS) != 0; } - public int getMoreKeyLabelFlags() { + public final int getMoreKeyLabelFlags() { return hasLabelsInMoreKeys() ? LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO : LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO; } - public boolean needsDividersInMoreKeys() { + public final boolean needsDividersInMoreKeys() { return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_NEEDS_DIVIDERS) != 0; } - public boolean hasEmbeddedMoreKey() { + public final boolean hasEmbeddedMoreKey() { return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_EMBEDDED_MORE_KEY) != 0; } - public Drawable getIcon(KeyboardIconsSet iconSet, int alpha) { - final int iconId = mEnabled ? mIconId : mDisabledIconId; + public final String getOutputText() { + final OptionalAttributes attrs = mOptionalAttributes; + return (attrs != null) ? attrs.mOutputText : null; + } + + public final int getAltCode() { + final OptionalAttributes attrs = mOptionalAttributes; + return (attrs != null) ? attrs.mAltCode : CODE_UNSPECIFIED; + } + + public Drawable getIcon(final KeyboardIconsSet iconSet, final int alpha) { + final OptionalAttributes attrs = mOptionalAttributes; + final int disabledIconId = (attrs != null) ? attrs.mDisabledIconId : ICON_UNDEFINED; + final int iconId = mEnabled ? mIconId : disabledIconId; final Drawable icon = iconSet.getIconDrawable(iconId); if (icon != null) { icon.setAlpha(alpha); @@ -586,10 +651,22 @@ public class Key { return icon; } - public Drawable getPreviewIcon(KeyboardIconsSet iconSet) { - return mPreviewIconId != ICON_UNDEFINED - ? iconSet.getIconDrawable(mPreviewIconId) - : iconSet.getIconDrawable(mIconId); + public Drawable getPreviewIcon(final KeyboardIconsSet iconSet) { + final OptionalAttributes attrs = mOptionalAttributes; + final int previewIconId = (attrs != null) ? attrs.mPreviewIconId : ICON_UNDEFINED; + return previewIconId != ICON_UNDEFINED + ? iconSet.getIconDrawable(previewIconId) : iconSet.getIconDrawable(mIconId); + } + + public final int getDrawX() { + final OptionalAttributes attrs = mOptionalAttributes; + return (attrs == null) ? mX : mX + attrs.mVisualInsetsLeft; + } + + public final int getDrawWidth() { + final OptionalAttributes attrs = mOptionalAttributes; + return (attrs == null) ? mWidth + : mWidth - attrs.mVisualInsetsLeft - attrs.mVisualInsetsRight; } /** @@ -610,11 +687,11 @@ public class Key { mPressed = false; } - public boolean isEnabled() { + public final boolean isEnabled() { return mEnabled; } - public void setEnabled(boolean enabled) { + public void setEnabled(final boolean enabled) { mEnabled = enabled; } @@ -624,9 +701,9 @@ public class Key { * @param y the y-coordinate of the point * @return whether or not the point falls on the key. If the key is attached to an edge, it * will assume that all points between the key and the edge are considered to be on the key. - * @see #markAsLeftEdge(Keyboard.Params) etc. + * @see #markAsLeftEdge(KeyboardParams) etc. */ - public boolean isOnKey(int x, int y) { + public boolean isOnKey(final int x, final int y) { return mHitBox.contains(x, y); } @@ -636,7 +713,7 @@ public class Key { * @param y the y-coordinate of the point * @return the square of the distance of the point from the nearest edge of the key */ - public int squaredDistanceToEdge(int x, int y) { + public int squaredDistanceToEdge(final int x, final int y) { final int left = mX; final int right = left + mWidth; final int top = mY; @@ -702,7 +779,7 @@ public class Key { * @return the drawable state of the key. * @see android.graphics.drawable.StateListDrawable#setState(int[]) */ - public int[] getCurrentDrawableState() { + public final int[] getCurrentDrawableState() { switch (mBackgroundType) { case BACKGROUND_TYPE_FUNCTIONAL: return mPressed ? KEY_STATE_FUNCTIONAL_PRESSED : KEY_STATE_FUNCTIONAL_NORMAL; @@ -718,15 +795,16 @@ public class Key { } public static class Spacer extends Key { - public Spacer(Resources res, Keyboard.Params params, Keyboard.Builder.Row row, - XmlPullParser parser) throws XmlPullParserException { + public Spacer(final Resources res, final KeyboardParams params, final KeyboardRow row, + final XmlPullParser parser) throws XmlPullParserException { super(res, params, row, parser); } /** * This constructor is being used only for divider in more keys keyboard. */ - protected Spacer(Keyboard.Params params, int x, int y, int width, int height) { + protected Spacer(final KeyboardParams params, final int x, final int y, final int width, + final int height) { super(params, null, null, ICON_UNDEFINED, CODE_UNSPECIFIED, null, x, y, width, height, 0); } diff --git a/java/src/com/android/inputmethod/keyboard/KeyDetector.java b/java/src/com/android/inputmethod/keyboard/KeyDetector.java index c0e6aa8d7..868c8cab5 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyDetector.java +++ b/java/src/com/android/inputmethod/keyboard/KeyDetector.java @@ -16,10 +16,10 @@ package com.android.inputmethod.keyboard; +import com.android.inputmethod.latin.Constants; -public class KeyDetector { - public static final int NOT_A_CODE = -1; +public class KeyDetector { private final int mKeyHysteresisDistanceSquared; private Keyboard mKeyboard; @@ -103,7 +103,7 @@ public class KeyDetector { final StringBuilder sb = new StringBuilder(); boolean addDelimiter = false; for (final int code : codes) { - if (code == NOT_A_CODE) break; + if (code == Constants.NOT_A_CODE) break; if (addDelimiter) sb.append(", "); sb.append(Keyboard.printableCode(code)); addDelimiter = true; diff --git a/java/src/com/android/inputmethod/keyboard/Keyboard.java b/java/src/com/android/inputmethod/keyboard/Keyboard.java index 919850095..261d1eba7 100644 --- a/java/src/com/android/inputmethod/keyboard/Keyboard.java +++ b/java/src/com/android/inputmethod/keyboard/Keyboard.java @@ -16,38 +16,15 @@ package com.android.inputmethod.keyboard; -import android.content.Context; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.content.res.XmlResourceParser; -import android.util.AttributeSet; -import android.util.DisplayMetrics; import android.util.Log; import android.util.SparseArray; -import android.util.SparseIntArray; -import android.util.TypedValue; -import android.util.Xml; -import android.view.InflateException; -import com.android.inputmethod.keyboard.internal.KeyStyles; -import com.android.inputmethod.keyboard.internal.KeyboardCodesSet; +import com.android.inputmethod.keyboard.internal.KeyVisualAttributes; import com.android.inputmethod.keyboard.internal.KeyboardIconsSet; -import com.android.inputmethod.keyboard.internal.KeyboardTextsSet; -import com.android.inputmethod.latin.LatinImeLogger; -import com.android.inputmethod.latin.LocaleUtils.RunInLocale; -import com.android.inputmethod.latin.R; -import com.android.inputmethod.latin.SubtypeLocale; -import com.android.inputmethod.latin.Utils; -import com.android.inputmethod.latin.XmlParseUtils; +import com.android.inputmethod.keyboard.internal.KeyboardParams; +import com.android.inputmethod.latin.CollectionUtils; -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Locale; /** * Loads an XML description of a keyboard and stores the attributes of the keys. A keyboard @@ -119,6 +96,9 @@ public class Keyboard { /** Default gap between rows */ public final int mVerticalGap; + /** Per keyboard key visual parameters */ + public final KeyVisualAttributes mKeyVisualAttributes; + public final int mMostCommonKeyHeight; public final int mMostCommonKeyWidth; @@ -134,12 +114,12 @@ public class Keyboard { public final Key[] mAltCodeKeysWhileTyping; public final KeyboardIconsSet mIconsSet; - private final SparseArray<Key> mKeyCache = new SparseArray<Key>(); + private final SparseArray<Key> mKeyCache = CollectionUtils.newSparseArray(); private final ProximityInfo mProximityInfo; private final boolean mProximityCharsCorrectionEnabled; - public Keyboard(Params params) { + public Keyboard(final KeyboardParams params) { mId = params.mId; mThemeId = params.mThemeId; mOccupiedHeight = params.mOccupiedHeight; @@ -148,7 +128,7 @@ public class Keyboard { mMostCommonKeyWidth = params.mMostCommonKeyWidth; mMoreKeysTemplate = params.mMoreKeysTemplate; mMaxMoreKeysKeyboardColumn = params.mMaxMoreKeysKeyboardColumn; - + mKeyVisualAttributes = params.mKeyVisualAttributes; mTopPadding = params.mTopPadding; mVerticalGap = params.mVerticalGap; @@ -164,7 +144,7 @@ public class Keyboard { mProximityCharsCorrectionEnabled = params.mProximityCharsCorrectionEnabled; } - public boolean hasProximityCharsCorrection(int code) { + public boolean hasProximityCharsCorrection(final int code) { if (!mProximityCharsCorrectionEnabled) { return false; } @@ -180,7 +160,7 @@ public class Keyboard { return mProximityInfo; } - public Key getKey(int code) { + public Key getKey(final int code) { if (code == CODE_UNSPECIFIED) { return null; } @@ -201,7 +181,7 @@ public class Keyboard { } } - public boolean hasKey(Key aKey) { + public boolean hasKey(final Key aKey) { if (mKeyCache.indexOfValue(aKey) >= 0) { return true; } @@ -215,7 +195,7 @@ public class Keyboard { return false; } - public static boolean isLetterCode(int code) { + public static boolean isLetterCode(final int code) { return code >= CODE_SPACE; } @@ -224,170 +204,6 @@ public class Keyboard { return mId.toString(); } - public static class Params { - public KeyboardId mId; - public int mThemeId; - - /** Total height and width of the keyboard, including the paddings and keys */ - public int mOccupiedHeight; - public int mOccupiedWidth; - - /** Base height and width of the keyboard used to calculate rows' or keys' heights and - * widths - */ - public int mBaseHeight; - public int mBaseWidth; - - public int mTopPadding; - public int mBottomPadding; - public int mHorizontalEdgesPadding; - public int mHorizontalCenterPadding; - - public int mDefaultRowHeight; - public int mDefaultKeyWidth; - public int mHorizontalGap; - public int mVerticalGap; - - public int mMoreKeysTemplate; - public int mMaxMoreKeysKeyboardColumn; - - public int GRID_WIDTH; - public int GRID_HEIGHT; - - public final HashSet<Key> mKeys = new HashSet<Key>(); - public final ArrayList<Key> mShiftKeys = new ArrayList<Key>(); - public final ArrayList<Key> mAltCodeKeysWhileTyping = new ArrayList<Key>(); - public final KeyboardIconsSet mIconsSet = new KeyboardIconsSet(); - public final KeyboardCodesSet mCodesSet = new KeyboardCodesSet(); - public final KeyboardTextsSet mTextsSet = new KeyboardTextsSet(); - public final KeyStyles mKeyStyles = new KeyStyles(mTextsSet); - - public KeyboardLayoutSet.KeysCache mKeysCache; - - public int mMostCommonKeyHeight = 0; - public int mMostCommonKeyWidth = 0; - - public boolean mProximityCharsCorrectionEnabled; - - public final TouchPositionCorrection mTouchPositionCorrection = - new TouchPositionCorrection(); - - public static class TouchPositionCorrection { - private static final int TOUCH_POSITION_CORRECTION_RECORD_SIZE = 3; - - public boolean mEnabled; - public float[] mXs; - public float[] mYs; - public float[] mRadii; - - public void load(String[] data) { - final int dataLength = data.length; - if (dataLength % TOUCH_POSITION_CORRECTION_RECORD_SIZE != 0) { - if (LatinImeLogger.sDBG) - throw new RuntimeException( - "the size of touch position correction data is invalid"); - return; - } - - final int length = dataLength / TOUCH_POSITION_CORRECTION_RECORD_SIZE; - mXs = new float[length]; - mYs = new float[length]; - mRadii = new float[length]; - try { - for (int i = 0; i < dataLength; ++i) { - final int type = i % TOUCH_POSITION_CORRECTION_RECORD_SIZE; - final int index = i / TOUCH_POSITION_CORRECTION_RECORD_SIZE; - final float value = Float.parseFloat(data[i]); - if (type == 0) { - mXs[index] = value; - } else if (type == 1) { - mYs[index] = value; - } else { - mRadii[index] = value; - } - } - } catch (NumberFormatException e) { - if (LatinImeLogger.sDBG) { - throw new RuntimeException( - "the number format for touch position correction data is invalid"); - } - mXs = null; - mYs = null; - mRadii = null; - } - } - - // TODO: Remove this method. - public void setEnabled(boolean enabled) { - mEnabled = enabled; - } - - public boolean isValid() { - return mEnabled && mXs != null && mYs != null && mRadii != null - && mXs.length > 0 && mYs.length > 0 && mRadii.length > 0; - } - } - - protected void clearKeys() { - mKeys.clear(); - mShiftKeys.clear(); - clearHistogram(); - } - - public void onAddKey(Key newKey) { - final Key key = (mKeysCache != null) ? mKeysCache.get(newKey) : newKey; - final boolean zeroWidthSpacer = key.isSpacer() && key.mWidth == 0; - if (!zeroWidthSpacer) { - mKeys.add(key); - updateHistogram(key); - } - if (key.mCode == Keyboard.CODE_SHIFT) { - mShiftKeys.add(key); - } - if (key.altCodeWhileTyping()) { - mAltCodeKeysWhileTyping.add(key); - } - } - - private int mMaxHeightCount = 0; - private int mMaxWidthCount = 0; - private final SparseIntArray mHeightHistogram = new SparseIntArray(); - private final SparseIntArray mWidthHistogram = new SparseIntArray(); - - private void clearHistogram() { - mMostCommonKeyHeight = 0; - mMaxHeightCount = 0; - mHeightHistogram.clear(); - - mMaxWidthCount = 0; - mMostCommonKeyWidth = 0; - mWidthHistogram.clear(); - } - - private static int updateHistogramCounter(SparseIntArray histogram, int key) { - final int index = histogram.indexOfKey(key); - final int count = (index >= 0 ? histogram.get(key) : 0) + 1; - histogram.put(key, count); - return count; - } - - private void updateHistogram(Key key) { - final int height = key.mHeight + key.mVerticalGap; - final int heightCount = updateHistogramCounter(mHeightHistogram, height); - if (heightCount > mMaxHeightCount) { - mMaxHeightCount = heightCount; - mMostCommonKeyHeight = height; - } - - final int width = key.mWidth + key.mHorizontalGap; - final int widthCount = updateHistogramCounter(mWidthHistogram, width); - if (widthCount > mMaxWidthCount) { - mMaxWidthCount = widthCount; - mMostCommonKeyWidth = width; - } - } - } - /** * Returns the array of the keys that are closest to the given point. * @param x the x-coordinate of the point @@ -395,14 +211,14 @@ public class Keyboard { * @return the array of the nearest keys to the given point. If the given * point is out of range, then an array of size zero is returned. */ - public Key[] getNearestKeys(int x, int y) { + public Key[] getNearestKeys(final int x, final int y) { // Avoid dead pixels at edges of the keyboard final int adjustedX = Math.max(0, Math.min(x, mOccupiedWidth - 1)); final int adjustedY = Math.max(0, Math.min(y, mOccupiedHeight - 1)); return mProximityInfo.getNearestKeys(adjustedX, adjustedY); } - public static String printableCode(int code) { + public static String printableCode(final int code) { switch (code) { case CODE_SHIFT: return "shift"; case CODE_SWITCH_ALPHA_SYMBOL: return "symbol"; @@ -424,934 +240,4 @@ public class Keyboard { return String.format("'\\u%04x'", code); } } - - /** - * Keyboard Building helper. - * - * This class parses Keyboard XML file and eventually build a Keyboard. - * The Keyboard XML file looks like: - * <pre> - * <!-- xml/keyboard.xml --> - * <Keyboard keyboard_attributes*> - * <!-- Keyboard Content --> - * <Row row_attributes*> - * <!-- Row Content --> - * <Key key_attributes* /> - * <Spacer horizontalGap="32.0dp" /> - * <include keyboardLayout="@xml/other_keys"> - * ... - * </Row> - * <include keyboardLayout="@xml/other_rows"> - * ... - * </Keyboard> - * </pre> - * The XML file which is included in other file must have <merge> as root element, - * such as: - * <pre> - * <!-- xml/other_keys.xml --> - * <merge> - * <Key key_attributes* /> - * ... - * </merge> - * </pre> - * and - * <pre> - * <!-- xml/other_rows.xml --> - * <merge> - * <Row row_attributes*> - * <Key key_attributes* /> - * </Row> - * ... - * </merge> - * </pre> - * You can also use switch-case-default tags to select Rows and Keys. - * <pre> - * <switch> - * <case case_attribute*> - * <!-- Any valid tags at switch position --> - * </case> - * ... - * <default> - * <!-- Any valid tags at switch position --> - * </default> - * </switch> - * </pre> - * You can declare Key style and specify styles within Key tags. - * <pre> - * <switch> - * <case mode="email"> - * <key-style styleName="f1-key" parentStyle="modifier-key" - * keyLabel=".com" - * /> - * </case> - * <case mode="url"> - * <key-style styleName="f1-key" parentStyle="modifier-key" - * keyLabel="http://" - * /> - * </case> - * </switch> - * ... - * <Key keyStyle="shift-key" ... /> - * </pre> - */ - - public static class Builder<KP extends Params> { - private static final String BUILDER_TAG = "Keyboard.Builder"; - private static final boolean DEBUG = false; - - // Keyboard XML Tags - private static final String TAG_KEYBOARD = "Keyboard"; - private static final String TAG_ROW = "Row"; - private static final String TAG_KEY = "Key"; - private static final String TAG_SPACER = "Spacer"; - private static final String TAG_INCLUDE = "include"; - private static final String TAG_MERGE = "merge"; - private static final String TAG_SWITCH = "switch"; - private static final String TAG_CASE = "case"; - private static final String TAG_DEFAULT = "default"; - public static final String TAG_KEY_STYLE = "key-style"; - - private static final int DEFAULT_KEYBOARD_COLUMNS = 10; - private static final int DEFAULT_KEYBOARD_ROWS = 4; - - protected final KP mParams; - protected final Context mContext; - protected final Resources mResources; - private final DisplayMetrics mDisplayMetrics; - - private int mCurrentY = 0; - private Row mCurrentRow = null; - private boolean mLeftEdge; - private boolean mTopEdge; - private Key mRightEdgeKey = null; - - /** - * Container for keys in the keyboard. All keys in a row are at the same Y-coordinate. - * Some of the key size defaults can be overridden per row from what the {@link Keyboard} - * defines. - */ - public static class Row { - // keyWidth enum constants - private static final int KEYWIDTH_NOT_ENUM = 0; - private static final int KEYWIDTH_FILL_RIGHT = -1; - - private final Params mParams; - /** Default width of a key in this row. */ - private float mDefaultKeyWidth; - /** Default height of a key in this row. */ - public final int mRowHeight; - /** Default keyLabelFlags in this row. */ - private int mDefaultKeyLabelFlags; - /** Default backgroundType for this row */ - private int mDefaultBackgroundType; - - private final int mCurrentY; - // Will be updated by {@link Key}'s constructor. - private float mCurrentX; - - public Row(Resources res, Params params, XmlPullParser parser, int y) { - mParams = params; - TypedArray keyboardAttr = res.obtainAttributes(Xml.asAttributeSet(parser), - R.styleable.Keyboard); - mRowHeight = (int)Builder.getDimensionOrFraction(keyboardAttr, - R.styleable.Keyboard_rowHeight, - params.mBaseHeight, params.mDefaultRowHeight); - keyboardAttr.recycle(); - TypedArray keyAttr = res.obtainAttributes(Xml.asAttributeSet(parser), - R.styleable.Keyboard_Key); - mDefaultKeyWidth = Builder.getDimensionOrFraction(keyAttr, - R.styleable.Keyboard_Key_keyWidth, - params.mBaseWidth, params.mDefaultKeyWidth); - mDefaultBackgroundType = keyAttr.getInt(R.styleable.Keyboard_Key_backgroundType, - Key.BACKGROUND_TYPE_NORMAL); - keyAttr.recycle(); - - // TODO: Initialize this with <Row> attribute as backgroundType is done. - mDefaultKeyLabelFlags = 0; - mCurrentY = y; - mCurrentX = 0.0f; - } - - public float getDefaultKeyWidth() { - return mDefaultKeyWidth; - } - - public void setDefaultKeyWidth(float defaultKeyWidth) { - mDefaultKeyWidth = defaultKeyWidth; - } - - public int getDefaultKeyLabelFlags() { - return mDefaultKeyLabelFlags; - } - - public void setDefaultKeyLabelFlags(int keyLabelFlags) { - mDefaultKeyLabelFlags = keyLabelFlags; - } - - public int getDefaultBackgroundType() { - return mDefaultBackgroundType; - } - - public void setDefaultBackgroundType(int backgroundType) { - mDefaultBackgroundType = backgroundType; - } - - public void setXPos(float keyXPos) { - mCurrentX = keyXPos; - } - - public void advanceXPos(float width) { - mCurrentX += width; - } - - public int getKeyY() { - return mCurrentY; - } - - public float getKeyX(TypedArray keyAttr) { - final int keyboardRightEdge = mParams.mOccupiedWidth - - mParams.mHorizontalEdgesPadding; - if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyXPos)) { - final float keyXPos = Builder.getDimensionOrFraction(keyAttr, - R.styleable.Keyboard_Key_keyXPos, 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. - return Math.max(keyXPos + keyboardRightEdge, mCurrentX); - } else { - return keyXPos + mParams.mHorizontalEdgesPadding; - } - } - return mCurrentX; - } - - public float getKeyWidth(TypedArray keyAttr) { - return getKeyWidth(keyAttr, mCurrentX); - } - - public float getKeyWidth(TypedArray keyAttr, float keyXPos) { - final int widthType = Builder.getEnumValue(keyAttr, - 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. - return keyboardRightEdge - keyXPos; - default: // KEYWIDTH_NOT_ENUM - return Builder.getDimensionOrFraction(keyAttr, - R.styleable.Keyboard_Key_keyWidth, - mParams.mBaseWidth, mDefaultKeyWidth); - } - } - } - - public Builder(Context context, KP params) { - mContext = context; - final Resources res = context.getResources(); - mResources = res; - mDisplayMetrics = res.getDisplayMetrics(); - - mParams = params; - - params.GRID_WIDTH = res.getInteger(R.integer.config_keyboard_grid_width); - params.GRID_HEIGHT = res.getInteger(R.integer.config_keyboard_grid_height); - } - - public void setAutoGenerate(KeyboardLayoutSet.KeysCache keysCache) { - mParams.mKeysCache = keysCache; - } - - public Builder<KP> load(int xmlId, KeyboardId id) { - mParams.mId = id; - final XmlResourceParser parser = mResources.getXml(xmlId); - try { - parseKeyboard(parser); - } catch (XmlPullParserException e) { - Log.w(BUILDER_TAG, "keyboard XML parse error: " + e); - throw new IllegalArgumentException(e); - } catch (IOException e) { - Log.w(BUILDER_TAG, "keyboard XML parse error: " + e); - throw new RuntimeException(e); - } finally { - parser.close(); - } - return this; - } - - // TODO: Remove this method. - public void setTouchPositionCorrectionEnabled(boolean enabled) { - mParams.mTouchPositionCorrection.setEnabled(enabled); - } - - public void setProximityCharsCorrectionEnabled(boolean enabled) { - mParams.mProximityCharsCorrectionEnabled = enabled; - } - - public Keyboard build() { - return new Keyboard(mParams); - } - - private int mIndent; - private static final String SPACES = " "; - - private static String spaces(int count) { - return (count < SPACES.length()) ? SPACES.substring(0, count) : SPACES; - } - - private void startTag(String format, Object ... args) { - Log.d(BUILDER_TAG, String.format(spaces(++mIndent * 2) + format, args)); - } - - private void endTag(String format, Object ... args) { - Log.d(BUILDER_TAG, String.format(spaces(mIndent-- * 2) + format, args)); - } - - private void startEndTag(String format, Object ... args) { - Log.d(BUILDER_TAG, String.format(spaces(++mIndent * 2) + format, args)); - mIndent--; - } - - private void parseKeyboard(XmlPullParser parser) - throws XmlPullParserException, IOException { - if (DEBUG) startTag("<%s> %s", TAG_KEYBOARD, mParams.mId); - int event; - while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) { - if (event == XmlPullParser.START_TAG) { - final String tag = parser.getName(); - if (TAG_KEYBOARD.equals(tag)) { - parseKeyboardAttributes(parser); - startKeyboard(); - parseKeyboardContent(parser, false); - break; - } else { - throw new XmlParseUtils.IllegalStartTag(parser, TAG_KEYBOARD); - } - } - } - } - - private void parseKeyboardAttributes(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 = Utils.getDeviceOverrideValue( - mResources, R.array.keyboard_heights, null); - final float keyboardHeight; - if (keyboardHeightString != null) { - keyboardHeight = Float.parseFloat(keyboardHeightString) - * mDisplayMetrics.density; - } else { - keyboardHeight = keyboardAttr.getDimension( - R.styleable.Keyboard_keyboardHeight, displayHeight / 2); - } - final float maxKeyboardHeight = getDimensionOrFraction(keyboardAttr, - R.styleable.Keyboard_maxKeyboardHeight, displayHeight, displayHeight / 2); - float minKeyboardHeight = 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 = -getDimensionOrFraction(keyboardAttr, - R.styleable.Keyboard_minKeyboardHeight, displayWidth, displayWidth / 2); - } - final Params 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)getDimensionOrFraction(keyboardAttr, - R.styleable.Keyboard_keyboardTopPadding, params.mOccupiedHeight, 0); - params.mBottomPadding = (int)getDimensionOrFraction(keyboardAttr, - R.styleable.Keyboard_keyboardBottomPadding, params.mOccupiedHeight, 0); - params.mHorizontalEdgesPadding = (int)getDimensionOrFraction(keyboardAttr, - R.styleable.Keyboard_keyboardHorizontalEdgesPadding, - mParams.mOccupiedWidth, 0); - - params.mBaseWidth = params.mOccupiedWidth - params.mHorizontalEdgesPadding * 2 - - params.mHorizontalCenterPadding; - params.mDefaultKeyWidth = (int)getDimensionOrFraction(keyAttr, - R.styleable.Keyboard_Key_keyWidth, params.mBaseWidth, - params.mBaseWidth / DEFAULT_KEYBOARD_COLUMNS); - params.mHorizontalGap = (int)getDimensionOrFraction(keyboardAttr, - R.styleable.Keyboard_horizontalGap, params.mBaseWidth, 0); - params.mVerticalGap = (int)getDimensionOrFraction(keyboardAttr, - R.styleable.Keyboard_verticalGap, params.mOccupiedHeight, 0); - params.mBaseHeight = params.mOccupiedHeight - params.mTopPadding - - params.mBottomPadding + params.mVerticalGap; - params.mDefaultRowHeight = (int)getDimensionOrFraction(keyboardAttr, - R.styleable.Keyboard_rowHeight, params.mBaseHeight, - params.mBaseHeight / DEFAULT_KEYBOARD_ROWS); - - params.mMoreKeysTemplate = keyboardAttr.getResourceId( - R.styleable.Keyboard_moreKeysTemplate, 0); - params.mMaxMoreKeysKeyboardColumn = keyAttr.getInt( - R.styleable.Keyboard_Key_maxMoreKeysColumn, 5); - - params.mThemeId = keyboardAttr.getInt(R.styleable.Keyboard_themeId, 0); - params.mIconsSet.loadIcons(keyboardAttr); - final String language = params.mId.mLocale.getLanguage(); - params.mCodesSet.setLanguage(language); - params.mTextsSet.setLanguage(language); - final RunInLocale<Void> job = new RunInLocale<Void>() { - @Override - protected Void job(Resources res) { - params.mTextsSet.loadStringResources(mContext); - return null; - } - }; - // Null means the current system locale. - final Locale locale = SubtypeLocale.isNoLanguage(params.mId.mSubtype) - ? null : params.mId.mLocale; - job.runInLocale(mResources, locale); - - final int resourceId = keyboardAttr.getResourceId( - R.styleable.Keyboard_touchPositionCorrectionData, 0); - params.mTouchPositionCorrection.setEnabled(resourceId != 0); - if (resourceId != 0) { - final String[] data = mResources.getStringArray(resourceId); - params.mTouchPositionCorrection.load(data); - } - } finally { - keyAttr.recycle(); - keyboardAttr.recycle(); - } - } - - private void parseKeyboardContent(XmlPullParser parser, boolean skip) - throws XmlPullParserException, IOException { - int event; - while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) { - if (event == XmlPullParser.START_TAG) { - final String tag = parser.getName(); - if (TAG_ROW.equals(tag)) { - Row row = parseRowAttributes(parser); - if (DEBUG) startTag("<%s>%s", TAG_ROW, skip ? " skipped" : ""); - if (!skip) { - startRow(row); - } - parseRowContent(parser, row, skip); - } else if (TAG_INCLUDE.equals(tag)) { - parseIncludeKeyboardContent(parser, skip); - } else if (TAG_SWITCH.equals(tag)) { - parseSwitchKeyboardContent(parser, skip); - } else if (TAG_KEY_STYLE.equals(tag)) { - parseKeyStyle(parser, skip); - } else { - throw new XmlParseUtils.IllegalStartTag(parser, TAG_ROW); - } - } else if (event == XmlPullParser.END_TAG) { - final String tag = parser.getName(); - if (DEBUG) endTag("</%s>", tag); - if (TAG_KEYBOARD.equals(tag)) { - endKeyboard(); - break; - } else if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag) - || TAG_MERGE.equals(tag)) { - break; - } else { - throw new XmlParseUtils.IllegalEndTag(parser, TAG_ROW); - } - } - } - } - - private Row parseRowAttributes(XmlPullParser parser) throws XmlPullParserException { - final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser), - R.styleable.Keyboard); - try { - if (a.hasValue(R.styleable.Keyboard_horizontalGap)) - throw new XmlParseUtils.IllegalAttribute(parser, "horizontalGap"); - if (a.hasValue(R.styleable.Keyboard_verticalGap)) - throw new XmlParseUtils.IllegalAttribute(parser, "verticalGap"); - return new Row(mResources, mParams, parser, mCurrentY); - } finally { - a.recycle(); - } - } - - private void parseRowContent(XmlPullParser parser, Row row, boolean skip) - throws XmlPullParserException, IOException { - int event; - while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) { - if (event == XmlPullParser.START_TAG) { - final String tag = parser.getName(); - if (TAG_KEY.equals(tag)) { - parseKey(parser, row, skip); - } else if (TAG_SPACER.equals(tag)) { - parseSpacer(parser, row, skip); - } else if (TAG_INCLUDE.equals(tag)) { - parseIncludeRowContent(parser, row, skip); - } else if (TAG_SWITCH.equals(tag)) { - parseSwitchRowContent(parser, row, skip); - } else if (TAG_KEY_STYLE.equals(tag)) { - parseKeyStyle(parser, skip); - } else { - throw new XmlParseUtils.IllegalStartTag(parser, TAG_KEY); - } - } else if (event == XmlPullParser.END_TAG) { - final String tag = parser.getName(); - if (DEBUG) endTag("</%s>", tag); - if (TAG_ROW.equals(tag)) { - if (!skip) { - endRow(row); - } - break; - } else if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag) - || TAG_MERGE.equals(tag)) { - break; - } else { - throw new XmlParseUtils.IllegalEndTag(parser, TAG_KEY); - } - } - } - } - - private void parseKey(XmlPullParser parser, Row row, boolean skip) - throws XmlPullParserException, IOException { - if (skip) { - XmlParseUtils.checkEndTag(TAG_KEY, parser); - if (DEBUG) startEndTag("<%s /> skipped", TAG_KEY); - } else { - final Key key = new Key(mResources, mParams, row, parser); - if (DEBUG) { - startEndTag("<%s%s %s moreKeys=%s />", TAG_KEY, - (key.isEnabled() ? "" : " disabled"), key, - Arrays.toString(key.mMoreKeys)); - } - XmlParseUtils.checkEndTag(TAG_KEY, parser); - endKey(key); - } - } - - private void parseSpacer(XmlPullParser parser, Row row, boolean skip) - throws XmlPullParserException, IOException { - if (skip) { - XmlParseUtils.checkEndTag(TAG_SPACER, parser); - if (DEBUG) startEndTag("<%s /> skipped", TAG_SPACER); - } else { - final Key.Spacer spacer = new Key.Spacer(mResources, mParams, row, parser); - if (DEBUG) startEndTag("<%s />", TAG_SPACER); - XmlParseUtils.checkEndTag(TAG_SPACER, parser); - endKey(spacer); - } - } - - private void parseIncludeKeyboardContent(XmlPullParser parser, boolean skip) - throws XmlPullParserException, IOException { - parseIncludeInternal(parser, null, skip); - } - - private void parseIncludeRowContent(XmlPullParser parser, Row row, boolean skip) - throws XmlPullParserException, IOException { - parseIncludeInternal(parser, row, skip); - } - - private void parseIncludeInternal(XmlPullParser parser, Row row, boolean skip) - throws XmlPullParserException, IOException { - if (skip) { - XmlParseUtils.checkEndTag(TAG_INCLUDE, parser); - if (DEBUG) startEndTag("</%s> skipped", TAG_INCLUDE); - } else { - final AttributeSet attr = Xml.asAttributeSet(parser); - final TypedArray keyboardAttr = mResources.obtainAttributes(attr, - R.styleable.Keyboard_Include); - final TypedArray keyAttr = mResources.obtainAttributes(attr, - R.styleable.Keyboard_Key); - int keyboardLayout = 0; - float savedDefaultKeyWidth = 0; - int savedDefaultKeyLabelFlags = 0; - int savedDefaultBackgroundType = Key.BACKGROUND_TYPE_NORMAL; - try { - XmlParseUtils.checkAttributeExists(keyboardAttr, - R.styleable.Keyboard_Include_keyboardLayout, "keyboardLayout", - TAG_INCLUDE, parser); - keyboardLayout = keyboardAttr.getResourceId( - R.styleable.Keyboard_Include_keyboardLayout, 0); - if (row != null) { - if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyXPos)) { - // Override current x coordinate. - row.setXPos(row.getKeyX(keyAttr)); - } - // TODO: Remove this if-clause and do the same as backgroundType below. - savedDefaultKeyWidth = row.getDefaultKeyWidth(); - if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyWidth)) { - // Override default key width. - row.setDefaultKeyWidth(row.getKeyWidth(keyAttr)); - } - savedDefaultKeyLabelFlags = row.getDefaultKeyLabelFlags(); - // Bitwise-or default keyLabelFlag if exists. - row.setDefaultKeyLabelFlags(keyAttr.getInt( - R.styleable.Keyboard_Key_keyLabelFlags, 0) - | savedDefaultKeyLabelFlags); - savedDefaultBackgroundType = row.getDefaultBackgroundType(); - // Override default backgroundType if exists. - row.setDefaultBackgroundType(keyAttr.getInt( - R.styleable.Keyboard_Key_backgroundType, - savedDefaultBackgroundType)); - } - } finally { - keyboardAttr.recycle(); - keyAttr.recycle(); - } - - XmlParseUtils.checkEndTag(TAG_INCLUDE, parser); - if (DEBUG) { - startEndTag("<%s keyboardLayout=%s />",TAG_INCLUDE, - mResources.getResourceEntryName(keyboardLayout)); - } - final XmlResourceParser parserForInclude = mResources.getXml(keyboardLayout); - try { - parseMerge(parserForInclude, row, skip); - } finally { - if (row != null) { - // Restore default keyWidth, keyLabelFlags, and backgroundType. - row.setDefaultKeyWidth(savedDefaultKeyWidth); - row.setDefaultKeyLabelFlags(savedDefaultKeyLabelFlags); - row.setDefaultBackgroundType(savedDefaultBackgroundType); - } - parserForInclude.close(); - } - } - } - - private void parseMerge(XmlPullParser parser, Row row, boolean skip) - throws XmlPullParserException, IOException { - if (DEBUG) startTag("<%s>", TAG_MERGE); - int event; - while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) { - if (event == XmlPullParser.START_TAG) { - final String tag = parser.getName(); - if (TAG_MERGE.equals(tag)) { - if (row == null) { - parseKeyboardContent(parser, skip); - } else { - parseRowContent(parser, row, skip); - } - break; - } else { - throw new XmlParseUtils.ParseException( - "Included keyboard layout must have <merge> root element", parser); - } - } - } - } - - private void parseSwitchKeyboardContent(XmlPullParser parser, boolean skip) - throws XmlPullParserException, IOException { - parseSwitchInternal(parser, null, skip); - } - - private void parseSwitchRowContent(XmlPullParser parser, Row row, boolean skip) - throws XmlPullParserException, IOException { - parseSwitchInternal(parser, row, skip); - } - - private void parseSwitchInternal(XmlPullParser parser, Row row, boolean skip) - throws XmlPullParserException, IOException { - if (DEBUG) startTag("<%s> %s", TAG_SWITCH, mParams.mId); - boolean selected = false; - int event; - while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) { - if (event == XmlPullParser.START_TAG) { - final String tag = parser.getName(); - if (TAG_CASE.equals(tag)) { - selected |= parseCase(parser, row, selected ? true : skip); - } else if (TAG_DEFAULT.equals(tag)) { - selected |= parseDefault(parser, row, selected ? true : skip); - } else { - throw new XmlParseUtils.IllegalStartTag(parser, TAG_KEY); - } - } else if (event == XmlPullParser.END_TAG) { - final String tag = parser.getName(); - if (TAG_SWITCH.equals(tag)) { - if (DEBUG) endTag("</%s>", TAG_SWITCH); - break; - } else { - throw new XmlParseUtils.IllegalEndTag(parser, TAG_KEY); - } - } - } - } - - private boolean parseCase(XmlPullParser parser, Row row, boolean skip) - throws XmlPullParserException, IOException { - final boolean selected = parseCaseCondition(parser); - if (row == null) { - // Processing Rows. - parseKeyboardContent(parser, selected ? skip : true); - } else { - // Processing Keys. - parseRowContent(parser, row, selected ? skip : true); - } - return selected; - } - - private boolean parseCaseCondition(XmlPullParser parser) { - final KeyboardId id = mParams.mId; - if (id == null) - return true; - - final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser), - R.styleable.Keyboard_Case); - try { - final boolean keyboardLayoutSetElementMatched = matchTypedValue(a, - R.styleable.Keyboard_Case_keyboardLayoutSetElement, id.mElementId, - KeyboardId.elementIdToName(id.mElementId)); - final boolean modeMatched = matchTypedValue(a, - R.styleable.Keyboard_Case_mode, id.mMode, KeyboardId.modeName(id.mMode)); - final boolean navigateNextMatched = matchBoolean(a, - R.styleable.Keyboard_Case_navigateNext, id.navigateNext()); - final boolean navigatePreviousMatched = matchBoolean(a, - R.styleable.Keyboard_Case_navigatePrevious, id.navigatePrevious()); - final boolean passwordInputMatched = matchBoolean(a, - R.styleable.Keyboard_Case_passwordInput, id.passwordInput()); - final boolean clobberSettingsKeyMatched = matchBoolean(a, - R.styleable.Keyboard_Case_clobberSettingsKey, id.mClobberSettingsKey); - final boolean shortcutKeyEnabledMatched = matchBoolean(a, - R.styleable.Keyboard_Case_shortcutKeyEnabled, id.mShortcutKeyEnabled); - final boolean hasShortcutKeyMatched = matchBoolean(a, - R.styleable.Keyboard_Case_hasShortcutKey, id.mHasShortcutKey); - final boolean languageSwitchKeyEnabledMatched = matchBoolean(a, - R.styleable.Keyboard_Case_languageSwitchKeyEnabled, - id.mLanguageSwitchKeyEnabled); - final boolean isMultiLineMatched = matchBoolean(a, - R.styleable.Keyboard_Case_isMultiLine, id.isMultiLine()); - final boolean imeActionMatched = matchInteger(a, - R.styleable.Keyboard_Case_imeAction, id.imeAction()); - final boolean localeCodeMatched = matchString(a, - R.styleable.Keyboard_Case_localeCode, id.mLocale.toString()); - final boolean languageCodeMatched = matchString(a, - R.styleable.Keyboard_Case_languageCode, id.mLocale.getLanguage()); - final boolean countryCodeMatched = matchString(a, - R.styleable.Keyboard_Case_countryCode, id.mLocale.getCountry()); - final boolean selected = keyboardLayoutSetElementMatched && modeMatched - && navigateNextMatched && navigatePreviousMatched && passwordInputMatched - && clobberSettingsKeyMatched && shortcutKeyEnabledMatched - && hasShortcutKeyMatched && languageSwitchKeyEnabledMatched - && isMultiLineMatched && imeActionMatched && localeCodeMatched - && languageCodeMatched && countryCodeMatched; - - if (DEBUG) { - startTag("<%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s>%s", TAG_CASE, - textAttr(a.getString( - R.styleable.Keyboard_Case_keyboardLayoutSetElement), - "keyboardLayoutSetElement"), - textAttr(a.getString(R.styleable.Keyboard_Case_mode), "mode"), - textAttr(a.getString(R.styleable.Keyboard_Case_imeAction), - "imeAction"), - booleanAttr(a, R.styleable.Keyboard_Case_navigateNext, - "navigateNext"), - booleanAttr(a, R.styleable.Keyboard_Case_navigatePrevious, - "navigatePrevious"), - booleanAttr(a, R.styleable.Keyboard_Case_clobberSettingsKey, - "clobberSettingsKey"), - booleanAttr(a, R.styleable.Keyboard_Case_passwordInput, - "passwordInput"), - booleanAttr(a, R.styleable.Keyboard_Case_shortcutKeyEnabled, - "shortcutKeyEnabled"), - booleanAttr(a, R.styleable.Keyboard_Case_hasShortcutKey, - "hasShortcutKey"), - booleanAttr(a, R.styleable.Keyboard_Case_languageSwitchKeyEnabled, - "languageSwitchKeyEnabled"), - booleanAttr(a, R.styleable.Keyboard_Case_isMultiLine, - "isMultiLine"), - textAttr(a.getString(R.styleable.Keyboard_Case_localeCode), - "localeCode"), - textAttr(a.getString(R.styleable.Keyboard_Case_languageCode), - "languageCode"), - textAttr(a.getString(R.styleable.Keyboard_Case_countryCode), - "countryCode"), - selected ? "" : " skipped"); - } - - return selected; - } finally { - a.recycle(); - } - } - - private static boolean matchInteger(TypedArray a, int index, int value) { - // If <case> does not have "index" attribute, that means this <case> is wild-card for - // the attribute. - return !a.hasValue(index) || a.getInt(index, 0) == value; - } - - private static boolean matchBoolean(TypedArray a, int index, boolean value) { - // If <case> does not have "index" attribute, that means this <case> is wild-card for - // the attribute. - return !a.hasValue(index) || a.getBoolean(index, false) == value; - } - - private static boolean matchString(TypedArray a, int index, String value) { - // If <case> does not have "index" attribute, that means this <case> is wild-card for - // the attribute. - return !a.hasValue(index) - || stringArrayContains(a.getString(index).split("\\|"), value); - } - - private static boolean matchTypedValue(TypedArray a, int index, int intValue, - String strValue) { - // If <case> does not have "index" attribute, that means this <case> is wild-card for - // the attribute. - final TypedValue v = a.peekValue(index); - if (v == null) - return true; - - if (isIntegerValue(v)) { - return intValue == a.getInt(index, 0); - } else if (isStringValue(v)) { - return stringArrayContains(a.getString(index).split("\\|"), strValue); - } - return false; - } - - private static boolean stringArrayContains(String[] array, String value) { - for (final String elem : array) { - if (elem.equals(value)) - return true; - } - return false; - } - - private boolean parseDefault(XmlPullParser parser, Row row, boolean skip) - throws XmlPullParserException, IOException { - if (DEBUG) startTag("<%s>", TAG_DEFAULT); - if (row == null) { - parseKeyboardContent(parser, skip); - } else { - parseRowContent(parser, row, skip); - } - return true; - } - - private void parseKeyStyle(XmlPullParser parser, boolean skip) - throws XmlPullParserException, IOException { - TypedArray keyStyleAttr = mResources.obtainAttributes(Xml.asAttributeSet(parser), - R.styleable.Keyboard_KeyStyle); - TypedArray keyAttrs = mResources.obtainAttributes(Xml.asAttributeSet(parser), - R.styleable.Keyboard_Key); - try { - if (!keyStyleAttr.hasValue(R.styleable.Keyboard_KeyStyle_styleName)) - throw new XmlParseUtils.ParseException("<" + TAG_KEY_STYLE - + "/> needs styleName attribute", parser); - if (DEBUG) { - startEndTag("<%s styleName=%s />%s", TAG_KEY_STYLE, - keyStyleAttr.getString(R.styleable.Keyboard_KeyStyle_styleName), - skip ? " skipped" : ""); - } - if (!skip) - mParams.mKeyStyles.parseKeyStyleAttributes(keyStyleAttr, keyAttrs, parser); - } finally { - keyStyleAttr.recycle(); - keyAttrs.recycle(); - } - XmlParseUtils.checkEndTag(TAG_KEY_STYLE, parser); - } - - private void startKeyboard() { - mCurrentY += mParams.mTopPadding; - mTopEdge = true; - } - - private void startRow(Row row) { - addEdgeSpace(mParams.mHorizontalEdgesPadding, row); - mCurrentRow = row; - mLeftEdge = true; - mRightEdgeKey = null; - } - - private void endRow(Row row) { - if (mCurrentRow == null) - throw new InflateException("orphan end row tag"); - if (mRightEdgeKey != null) { - mRightEdgeKey.markAsRightEdge(mParams); - mRightEdgeKey = null; - } - addEdgeSpace(mParams.mHorizontalEdgesPadding, row); - mCurrentY += row.mRowHeight; - mCurrentRow = null; - mTopEdge = false; - } - - private void endKey(Key key) { - mParams.onAddKey(key); - if (mLeftEdge) { - key.markAsLeftEdge(mParams); - mLeftEdge = false; - } - if (mTopEdge) { - key.markAsTopEdge(mParams); - } - mRightEdgeKey = key; - } - - private void endKeyboard() { - // nothing to do here. - } - - private void addEdgeSpace(float width, Row row) { - row.advanceXPos(width); - mLeftEdge = false; - mRightEdgeKey = null; - } - - public static float getDimensionOrFraction(TypedArray a, int index, int base, - float defValue) { - final TypedValue value = a.peekValue(index); - if (value == null) - return defValue; - if (isFractionValue(value)) { - return a.getFraction(index, base, base, defValue); - } else if (isDimensionValue(value)) { - return a.getDimension(index, defValue); - } - return defValue; - } - - public static int getEnumValue(TypedArray a, int index, int defValue) { - final TypedValue value = a.peekValue(index); - if (value == null) - return defValue; - if (isIntegerValue(value)) { - return a.getInt(index, defValue); - } - return defValue; - } - - private static boolean isFractionValue(TypedValue v) { - return v.type == TypedValue.TYPE_FRACTION; - } - - private static boolean isDimensionValue(TypedValue v) { - return v.type == TypedValue.TYPE_DIMENSION; - } - - private static boolean isIntegerValue(TypedValue v) { - return v.type >= TypedValue.TYPE_FIRST_INT && v.type <= TypedValue.TYPE_LAST_INT; - } - - private static boolean isStringValue(TypedValue v) { - return v.type == TypedValue.TYPE_STRING; - } - - private static String textAttr(String value, String name) { - return value != null ? String.format(" %s=%s", name, value) : ""; - } - - private static String booleanAttr(TypedArray a, int index, String name) { - return a.hasValue(index) - ? String.format(" %s=%s", name, a.getBoolean(index, false)) : ""; - } - } } diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java b/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java index b1621a55b..5c8f78f5e 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java +++ b/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java @@ -16,6 +16,7 @@ package com.android.inputmethod.keyboard; +import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.InputPointers; public interface KeyboardActionListener { @@ -44,21 +45,16 @@ public interface KeyboardActionListener { * * @param primaryCode this is the code of the key that was pressed * @param x x-coordinate pixel of touched event. If {@link #onCodeInput} is not called by - * {@link PointerTracker} or so, the value should be {@link #NOT_A_TOUCH_COORDINATE}. - * If it's called on insertion from the suggestion strip, it should be - * {@link #SUGGESTION_STRIP_COORDINATE}. + * {@link PointerTracker} or so, the value should be + * {@link Constants#NOT_A_COORDINATE}. If it's called on insertion from the + * suggestion strip, it should be {@link Constants#SUGGESTION_STRIP_COORDINATE}. * @param y y-coordinate pixel of touched event. If {@link #onCodeInput} is not called by - * {@link PointerTracker} or so, the value should be {@link #NOT_A_TOUCH_COORDINATE}. - * If it's called on insertion from the suggestion strip, it should be - * {@link #SUGGESTION_STRIP_COORDINATE}. + * {@link PointerTracker} or so, the value should be + * {@link Constants#NOT_A_COORDINATE}.If it's called on insertion from the + * suggestion strip, it should be {@link Constants#SUGGESTION_STRIP_COORDINATE}. */ public void onCodeInput(int primaryCode, int x, int y); - // See {@link Adapter#isInvalidCoordinate(int)}. - public static final int NOT_A_TOUCH_COORDINATE = -1; - public static final int SUGGESTION_STRIP_COORDINATE = -2; - public static final int SPELL_CHECKER_COORDINATE = -3; - /** * Sends a sequence of characters to the listener. * @@ -119,9 +115,9 @@ public interface KeyboardActionListener { // TODO: Remove this method when the vertical correction is removed. public static boolean isInvalidCoordinate(int coordinate) { - // Detect {@link KeyboardActionListener#NOT_A_TOUCH_COORDINATE}, - // {@link KeyboardActionListener#SUGGESTION_STRIP_COORDINATE}, and - // {@link KeyboardActionListener#SPELL_CHECKER_COORDINATE}. + // Detect {@link Constants#NOT_A_COORDINATE}, + // {@link Constants#SUGGESTION_STRIP_COORDINATE}, and + // {@link Constants#SPELL_CHECKER_COORDINATE}. return coordinate < 0; } } diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java index 64b3f0952..aaccf63ba 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java +++ b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java @@ -35,7 +35,10 @@ import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodSubtype; import com.android.inputmethod.compat.EditorInfoCompatUtils; -import com.android.inputmethod.keyboard.KeyboardLayoutSet.Params.ElementParams; +import com.android.inputmethod.keyboard.internal.KeyboardBuilder; +import com.android.inputmethod.keyboard.internal.KeyboardParams; +import com.android.inputmethod.keyboard.internal.KeysCache; +import com.android.inputmethod.latin.CollectionUtils; import com.android.inputmethod.latin.InputAttributes; import com.android.inputmethod.latin.InputTypeUtils; import com.android.inputmethod.latin.LatinImeLogger; @@ -71,41 +74,25 @@ public class KeyboardLayoutSet { private final Params mParams; private static final HashMap<KeyboardId, SoftReference<Keyboard>> sKeyboardCache = - new HashMap<KeyboardId, SoftReference<Keyboard>>(); + CollectionUtils.newHashMap(); private static final KeysCache sKeysCache = new KeysCache(); public static class KeyboardLayoutSetException extends RuntimeException { public final KeyboardId mKeyboardId; - public KeyboardLayoutSetException(Throwable cause, KeyboardId keyboardId) { + public KeyboardLayoutSetException(final Throwable cause, final KeyboardId keyboardId) { super(cause); mKeyboardId = keyboardId; } } - public static class KeysCache { - private final HashMap<Key, Key> mMap; - - public KeysCache() { - mMap = new HashMap<Key, Key>(); - } - - public void clear() { - mMap.clear(); - } - - public Key get(Key key) { - final Key existingKey = mMap.get(key); - if (existingKey != null) { - // Reuse the existing element that equals to "key" without adding "key" to the map. - return existingKey; - } - mMap.put(key, key); - return key; - } + private static class ElementParams { + int mKeyboardXmlId; + boolean mProximityCharsCorrectionEnabled; + public ElementParams() {} } - static class Params { + private static class Params { String mKeyboardLayoutSetName; int mMode; EditorInfo mEditorInfo; @@ -120,12 +107,8 @@ public class KeyboardLayoutSet { int mWidth; // Sparse array of KeyboardLayoutSet element parameters indexed by element's id. final SparseArray<ElementParams> mKeyboardLayoutSetElementIdToParamsMap = - new SparseArray<ElementParams>(); - - static class ElementParams { - int mKeyboardXmlId; - boolean mProximityCharsCorrectionEnabled; - } + CollectionUtils.newSparseArray(); + public Params() {} } public static void clearKeyboardCache() { @@ -133,12 +116,12 @@ public class KeyboardLayoutSet { sKeysCache.clear(); } - private KeyboardLayoutSet(Context context, Params params) { + KeyboardLayoutSet(final Context context, final Params params) { mContext = context; mParams = params; } - public Keyboard getKeyboard(int baseKeyboardLayoutSetElementId) { + public Keyboard getKeyboard(final int baseKeyboardLayoutSetElementId) { final int keyboardLayoutSetElementId; switch (mParams.mMode) { case KeyboardId.MODE_PHONE: @@ -173,12 +156,12 @@ public class KeyboardLayoutSet { } } - private Keyboard getKeyboard(ElementParams elementParams, final KeyboardId id) { + private Keyboard getKeyboard(final ElementParams elementParams, final KeyboardId id) { final SoftReference<Keyboard> ref = sKeyboardCache.get(id); Keyboard keyboard = (ref == null) ? null : ref.get(); if (keyboard == null) { - final Keyboard.Builder<Keyboard.Params> builder = - new Keyboard.Builder<Keyboard.Params>(mContext, new Keyboard.Params()); + final KeyboardBuilder<KeyboardParams> builder = + new KeyboardBuilder<KeyboardParams>(mContext, new KeyboardParams()); if (id.isAlphabetKeyboard()) { builder.setAutoGenerate(sKeysCache); } @@ -205,7 +188,7 @@ public class KeyboardLayoutSet { // KeyboardLayoutSet element id that is a key in keyboard_set.xml. Also that file specifies // which XML layout should be used for each keyboard. The KeyboardId is an internal key for // Keyboard object. - private KeyboardId getKeyboardId(int keyboardLayoutSetElementId) { + private KeyboardId getKeyboardId(final int keyboardLayoutSetElementId) { final Params params = mParams; final boolean isSymbols = (keyboardLayoutSetElementId == KeyboardId.ELEMENT_SYMBOLS || keyboardLayoutSetElementId == KeyboardId.ELEMENT_SYMBOLS_SHIFTED); @@ -228,7 +211,7 @@ public class KeyboardLayoutSet { private static final EditorInfo EMPTY_EDITOR_INFO = new EditorInfo(); - public Builder(Context context, EditorInfo editorInfo) { + public Builder(final Context context, final EditorInfo editorInfo) { mContext = context; mPackageName = context.getPackageName(); mResources = context.getResources(); @@ -241,7 +224,8 @@ public class KeyboardLayoutSet { mPackageName, NO_SETTINGS_KEY, mEditorInfo); } - public Builder setScreenGeometry(int deviceFormFactor, int orientation, int widthPixels) { + public Builder setScreenGeometry(final int deviceFormFactor, final int orientation, + final int widthPixels) { final Params params = mParams; params.mDeviceFormFactor = deviceFormFactor; params.mOrientation = orientation; @@ -249,7 +233,7 @@ public class KeyboardLayoutSet { return this; } - public Builder setSubtype(InputMethodSubtype subtype) { + public Builder setSubtype(final InputMethodSubtype subtype) { final boolean asciiCapable = subtype.containsExtraValueKey(ASCII_CAPABLE); @SuppressWarnings("deprecation") final boolean deprecatedForceAscii = InputAttributes.inPrivateImeOptions( @@ -266,8 +250,8 @@ public class KeyboardLayoutSet { return this; } - public Builder setOptions(boolean voiceKeyEnabled, boolean voiceKeyOnMain, - boolean languageSwitchKeyEnabled) { + public Builder setOptions(final boolean voiceKeyEnabled, final boolean voiceKeyOnMain, + final boolean languageSwitchKeyEnabled) { @SuppressWarnings("deprecation") final boolean deprecatedNoMicrophone = InputAttributes.inPrivateImeOptions( null, NO_MICROPHONE_COMPAT, mEditorInfo); @@ -280,7 +264,7 @@ public class KeyboardLayoutSet { return this; } - public void setTouchPositionCorrectionEnabled(boolean enabled) { + public void setTouchPositionCorrectionEnabled(final boolean enabled) { mParams.mTouchPositionCorrectionEnabled = enabled; } @@ -301,7 +285,7 @@ public class KeyboardLayoutSet { return new KeyboardLayoutSet(mContext, mParams); } - private void parseKeyboardLayoutSet(Resources res, int resId) + private void parseKeyboardLayoutSet(final Resources res, final int resId) throws XmlPullParserException, IOException { final XmlResourceParser parser = res.getXml(resId); try { @@ -321,7 +305,7 @@ public class KeyboardLayoutSet { } } - private void parseKeyboardLayoutSetContent(XmlPullParser parser) + private void parseKeyboardLayoutSetContent(final XmlPullParser parser) throws XmlPullParserException, IOException { int event; while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) { @@ -343,7 +327,7 @@ public class KeyboardLayoutSet { } } - private void parseKeyboardLayoutSetElement(XmlPullParser parser) + private void parseKeyboardLayoutSetElement(final XmlPullParser parser) throws XmlPullParserException, IOException { final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser), R.styleable.KeyboardLayoutSet_Element); @@ -370,7 +354,7 @@ public class KeyboardLayoutSet { } } - private static int getKeyboardMode(EditorInfo editorInfo) { + private static int getKeyboardMode(final EditorInfo editorInfo) { if (editorInfo == null) return KeyboardId.MODE_TEXT; diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java index 10f651ad1..fd789f029 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java +++ b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java @@ -21,7 +21,6 @@ import android.content.SharedPreferences; import android.content.res.Resources; import android.util.Log; import android.view.ContextThemeWrapper; -import android.view.InflateException; import android.view.LayoutInflater; import android.view.View; import android.view.inputmethod.EditorInfo; @@ -38,7 +37,7 @@ import com.android.inputmethod.latin.LatinImeLogger; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.SettingsValues; import com.android.inputmethod.latin.SubtypeSwitcher; -import com.android.inputmethod.latin.Utils; +import com.android.inputmethod.latin.WordComposer; public class KeyboardSwitcher implements KeyboardState.SwitchActions { private static final String TAG = KeyboardSwitcher.class.getSimpleName(); @@ -46,24 +45,24 @@ public class KeyboardSwitcher implements KeyboardState.SwitchActions { public static final String PREF_KEYBOARD_LAYOUT = "pref_keyboard_layout_20110916"; static class KeyboardTheme { - public final String mName; public final int mThemeId; public final int mStyleId; - public KeyboardTheme(String name, int themeId, int styleId) { - mName = name; + // Note: The themeId should be aligned with "themeId" attribute of Keyboard style + // in values/style.xml. + public KeyboardTheme(int themeId, int styleId) { mThemeId = themeId; mStyleId = styleId; } } private static final KeyboardTheme[] KEYBOARD_THEMES = { - new KeyboardTheme("Basic", 0, R.style.KeyboardTheme), - new KeyboardTheme("HighContrast", 1, R.style.KeyboardTheme_HighContrast), - new KeyboardTheme("Stone", 6, R.style.KeyboardTheme_Stone), - new KeyboardTheme("Stone.Bold", 7, R.style.KeyboardTheme_Stone_Bold), - new KeyboardTheme("GingerBread", 8, R.style.KeyboardTheme_Gingerbread), - new KeyboardTheme("IceCreamSandwich", 5, R.style.KeyboardTheme_IceCreamSandwich), + new KeyboardTheme(0, R.style.KeyboardTheme), + new KeyboardTheme(1, R.style.KeyboardTheme_HighContrast), + new KeyboardTheme(6, R.style.KeyboardTheme_Stone), + new KeyboardTheme(7, R.style.KeyboardTheme_Stone_Bold), + new KeyboardTheme(8, R.style.KeyboardTheme_Gingerbread), + new KeyboardTheme(5, R.style.KeyboardTheme_IceCreamSandwich), }; private SubtypeSwitcher mSubtypeSwitcher; @@ -354,22 +353,9 @@ public class KeyboardSwitcher implements KeyboardState.SwitchActions { mKeyboardView.closing(); } - Utils.GCUtils.getInstance().reset(); - boolean tryGC = true; - for (int i = 0; i < Utils.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) { - try { - setContextThemeWrapper(mLatinIME, mKeyboardTheme); - mCurrentInputView = (InputView)LayoutInflater.from(mThemeContext).inflate( - R.layout.input_view, null); - tryGC = false; - } catch (OutOfMemoryError e) { - Log.w(TAG, "load keyboard failed: " + e); - tryGC = Utils.GCUtils.getInstance().tryGCOrWait(mKeyboardTheme.mName, e); - } catch (InflateException e) { - Log.w(TAG, "load keyboard failed: " + e); - tryGC = Utils.GCUtils.getInstance().tryGCOrWait(mKeyboardTheme.mName, e); - } - } + setContextThemeWrapper(mLatinIME, mKeyboardTheme); + mCurrentInputView = (InputView)LayoutInflater.from(mThemeContext).inflate( + R.layout.input_view, null); mKeyboardView = (MainKeyboardView) mCurrentInputView.findViewById(R.id.keyboard_view); if (isHardwareAcceleratedDrawingEnabled) { @@ -402,4 +388,16 @@ public class KeyboardSwitcher implements KeyboardState.SwitchActions { } } } + + public int getManualCapsMode() { + switch (getKeyboard().mId.mElementId) { + case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED: + case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED: + return WordComposer.CAPS_MODE_MANUAL_SHIFT_LOCKED; + case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED: + return WordComposer.CAPS_MODE_MANUAL_SHIFTED; + default: + return WordComposer.CAPS_MODE_OFF; + } + } } diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardView.java b/java/src/com/android/inputmethod/keyboard/KeyboardView.java index fcf97b99c..5b02f9f5a 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyboardView.java +++ b/java/src/com/android/inputmethod/keyboard/KeyboardView.java @@ -30,6 +30,7 @@ import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.os.Message; import android.util.AttributeSet; +import android.util.Log; import android.util.SparseArray; import android.util.TypedValue; import android.view.LayoutInflater; @@ -37,7 +38,11 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; +import com.android.inputmethod.keyboard.internal.KeyDrawParams; +import com.android.inputmethod.keyboard.internal.KeyPreviewDrawParams; +import com.android.inputmethod.keyboard.internal.KeyVisualAttributes; import com.android.inputmethod.keyboard.internal.PreviewPlacerView; +import com.android.inputmethod.latin.CollectionUtils; import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.LatinImeLogger; import com.android.inputmethod.latin.R; @@ -51,39 +56,72 @@ import java.util.HashSet; /** * A view that renders a virtual {@link Keyboard}. * - * @attr ref R.styleable#KeyboardView_backgroundDimAlpha * @attr ref R.styleable#KeyboardView_keyBackground - * @attr ref R.styleable#KeyboardView_keyLetterRatio - * @attr ref R.styleable#KeyboardView_keyLargeLetterRatio - * @attr ref R.styleable#KeyboardView_keyLabelRatio - * @attr ref R.styleable#KeyboardView_keyHintLetterRatio - * @attr ref R.styleable#KeyboardView_keyShiftedLetterHintRatio - * @attr ref R.styleable#KeyboardView_keyHintLabelRatio + * @attr ref R.styleable#KeyboardView_moreKeysLayout + * @attr ref R.styleable#KeyboardView_keyPreviewLayout + * @attr ref R.styleable#KeyboardView_keyPreviewBackground + * @attr ref R.styleable#KeyboardView_keyPreviewLeftBackground + * @attr ref R.styleable#KeyboardView_keyPreviewRightBackground + * @attr ref R.styleable#KeyboardView_keyPreviewOffset + * @attr ref R.styleable#KeyboardView_keyPreviewHeight + * @attr ref R.styleable#KeyboardView_keyPreviewLingerTimeout * @attr ref R.styleable#KeyboardView_keyLabelHorizontalPadding * @attr ref R.styleable#KeyboardView_keyHintLetterPadding * @attr ref R.styleable#KeyboardView_keyPopupHintLetterPadding * @attr ref R.styleable#KeyboardView_keyShiftedLetterHintPadding - * @attr ref R.styleable#KeyboardView_keyTextStyle - * @attr ref R.styleable#KeyboardView_keyPreviewLayout - * @attr ref R.styleable#KeyboardView_keyPreviewTextRatio - * @attr ref R.styleable#KeyboardView_keyPreviewOffset - * @attr ref R.styleable#KeyboardView_keyPreviewHeight - * @attr ref R.styleable#KeyboardView_keyTextColor - * @attr ref R.styleable#KeyboardView_keyTextColorDisabled - * @attr ref R.styleable#KeyboardView_keyHintLetterColor - * @attr ref R.styleable#KeyboardView_keyHintLabelColor - * @attr ref R.styleable#KeyboardView_keyShiftedLetterHintInactivatedColor - * @attr ref R.styleable#KeyboardView_keyShiftedLetterHintActivatedColor - * @attr ref R.styleable#KeyboardView_shadowColor - * @attr ref R.styleable#KeyboardView_shadowRadius + * @attr ref R.styleable#KeyboardView_keyTextShadowRadius + * @attr ref R.styleable#KeyboardView_backgroundDimAlpha + * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewTextSize + * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewTextColor + * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewTextOffset + * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewTextShadingColor + * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewTextShadingBorder + * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewTextShadowColor + * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewTextShadowBorder + * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewTextConnectorColor + * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewTextConnectorWidth + * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewTextLingerTimeout + * @attr ref R.styleable#KeyboardView_gesturePreviewTrailFadeoutStartDelay + * @attr ref R.styleable#KeyboardView_gesturePreviewTrailFadeoutDuration + * @attr ref R.styleable#KeyboardView_gesturePreviewTrailUpdateInterval + * @attr ref R.styleable#KeyboardView_gesturePreviewTrailColor + * @attr ref R.styleable#KeyboardView_gesturePreviewTrailWidth + * @attr ref R.styleable#KeyboardView_verticalCorrection + * @attr ref R.styleable#Keyboard_Key_keyTypeface + * @attr ref R.styleable#Keyboard_Key_keyLetterSize + * @attr ref R.styleable#Keyboard_Key_keyLabelSize + * @attr ref R.styleable#Keyboard_Key_keyLargeLetterRatio + * @attr ref R.styleable#Keyboard_Key_keyLargeLabelRatio + * @attr ref R.styleable#Keyboard_Key_keyHintLetterRatio + * @attr ref R.styleable#Keyboard_Key_keyShiftedLetterHintRatio + * @attr ref R.styleable#Keyboard_Key_keyHintLabelRatio + * @attr ref R.styleable#Keyboard_Key_keyPreviewTextRatio + * @attr ref R.styleable#Keyboard_Key_keyTextColor + * @attr ref R.styleable#Keyboard_Key_keyTextColorDisabled + * @attr ref R.styleable#Keyboard_Key_keyTextShadowColor + * @attr ref R.styleable#Keyboard_Key_keyHintLetterColor + * @attr ref R.styleable#Keyboard_Key_keyHintLabelColor + * @attr ref R.styleable#Keyboard_Key_keyShiftedLetterHintInactivatedColor + * @attr ref R.styleable#Keyboard_Key_keyShiftedLetterHintActivatedColor + * @attr ref R.styleable#Keyboard_Key_keyPreviewTextColor */ public class KeyboardView extends View implements PointerTracker.DrawingProxy { + private static final String TAG = KeyboardView.class.getSimpleName(); + // Miscellaneous constants private static final int[] LONG_PRESSABLE_STATE_SET = { android.R.attr.state_long_pressable }; // XML attributes + private final KeyVisualAttributes mKeyVisualAttributes; + private final int mKeyLabelHorizontalPadding; + private final float mKeyHintLetterPadding; + private final float mKeyPopupHintLetterPadding; + private final float mKeyShiftedLetterHintPadding; + private final float mKeyTextShadowRadius; protected final float mVerticalCorrection; protected final int mMoreKeysLayout; + protected final Drawable mKeyBackground; + protected final Rect mKeyBackgroundPadding = new Rect(); private final int mBackgroundDimAlpha; // HORIZONTAL ELLIPSIS "...", character for popup hint. @@ -99,11 +137,19 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { // Main keyboard private Keyboard mKeyboard; - protected final KeyDrawParams mKeyDrawParams; + protected final KeyDrawParams mKeyDrawParams = new KeyDrawParams(); // Key preview + private static final int PREVIEW_ALPHA = 240; private final int mKeyPreviewLayoutId; - protected final KeyPreviewDrawParams mKeyPreviewDrawParams; + private final Drawable mPreviewBackground; + private final Drawable mPreviewLeftBackground; + private final Drawable mPreviewRightBackground; + private final int mPreviewOffset; + private final int mPreviewHeight; + private final int mPreviewLingerTimeout; + private final SparseArray<TextView> mKeyPreviewTexts = CollectionUtils.newSparseArray(); + protected final KeyPreviewDrawParams mKeyPreviewDrawParams = new KeyPreviewDrawParams(); private boolean mShowKeyPreviewPopup = true; private int mDelayAfterPreview; private final PreviewPlacerView mPreviewPlacerView; @@ -114,7 +160,7 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { /** True if all keys should be drawn */ private boolean mInvalidateAllKeys; /** The keys that should be drawn */ - private final HashSet<Key> mInvalidatedKeys = new HashSet<Key>(); + private final HashSet<Key> mInvalidatedKeys = CollectionUtils.newHashSet(); /** The working rectangle variable */ private final Rect mWorkingRect = new Rect(); /** The keyboard bitmap buffer for faster updates */ @@ -126,9 +172,9 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { private final Paint mPaint = new Paint(); private final Paint.FontMetrics mFontMetrics = new Paint.FontMetrics(); // This sparse array caches key label text height in pixel indexed by key label text size. - private static final SparseArray<Float> sTextHeightCache = new SparseArray<Float>(); + private static final SparseArray<Float> sTextHeightCache = CollectionUtils.newSparseArray(); // This sparse array caches key label text width in pixel indexed by key label text size. - private static final SparseArray<Float> sTextWidthCache = new SparseArray<Float>(); + private static final SparseArray<Float> sTextWidthCache = CollectionUtils.newSparseArray(); private static final char[] KEY_LABEL_REFERENCE_CHAR = { 'M' }; private static final char[] KEY_NUMERIC_HINT_LABEL_REFERENCE_CHAR = { '8' }; @@ -137,31 +183,34 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { public static class DrawingHandler extends StaticInnerHandlerWrapper<KeyboardView> { private static final int MSG_DISMISS_KEY_PREVIEW = 0; - public DrawingHandler(KeyboardView outerInstance) { + public DrawingHandler(final KeyboardView outerInstance) { super(outerInstance); } @Override - public void handleMessage(Message msg) { + public void handleMessage(final Message msg) { final KeyboardView keyboardView = getOuterInstance(); if (keyboardView == null) return; final PointerTracker tracker = (PointerTracker) msg.obj; switch (msg.what) { case MSG_DISMISS_KEY_PREVIEW: - tracker.getKeyPreviewText().setVisibility(View.INVISIBLE); + final TextView previewText = keyboardView.mKeyPreviewTexts.get(tracker.mPointerId); + if (previewText != null) { + previewText.setVisibility(INVISIBLE); + } break; } } - public void dismissKeyPreview(long delay, PointerTracker tracker) { + public void dismissKeyPreview(final long delay, final PointerTracker tracker) { sendMessageDelayed(obtainMessage(MSG_DISMISS_KEY_PREVIEW, tracker), delay); } - public void cancelDismissKeyPreview(PointerTracker tracker) { + public void cancelDismissKeyPreview(final PointerTracker tracker) { removeMessages(MSG_DISMISS_KEY_PREVIEW, tracker); } - public void cancelAllDismissKeyPreviews() { + private void cancelAllDismissKeyPreviews() { removeMessages(MSG_DISMISS_KEY_PREVIEW); } @@ -170,220 +219,74 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { } } - protected static class KeyDrawParams { - // XML attributes - public final int mKeyTextColor; - public final int mKeyTextInactivatedColor; - public final Typeface mKeyTextStyle; - public final float mKeyLabelHorizontalPadding; - public final float mKeyHintLetterPadding; - public final float mKeyPopupHintLetterPadding; - public final float mKeyShiftedLetterHintPadding; - public final int mShadowColor; - public final float mShadowRadius; - public final Drawable mKeyBackground; - public final int mKeyHintLetterColor; - public final int mKeyHintLabelColor; - public final int mKeyShiftedLetterHintInactivatedColor; - public final int mKeyShiftedLetterHintActivatedColor; - - /* package */ final float mKeyLetterRatio; - private final float mKeyLargeLetterRatio; - private final float mKeyLabelRatio; - private final float mKeyLargeLabelRatio; - private final float mKeyHintLetterRatio; - private final float mKeyShiftedLetterHintRatio; - private final float mKeyHintLabelRatio; - private static final float UNDEFINED_RATIO = -1.0f; - - public final Rect mPadding = new Rect(); - public int mKeyLetterSize; - public int mKeyLargeLetterSize; - public int mKeyLabelSize; - public int mKeyLargeLabelSize; - public int mKeyHintLetterSize; - public int mKeyShiftedLetterHintSize; - public int mKeyHintLabelSize; - public int mAnimAlpha; - - public KeyDrawParams(TypedArray a) { - mKeyBackground = a.getDrawable(R.styleable.KeyboardView_keyBackground); - if (a.hasValue(R.styleable.KeyboardView_keyLetterSize)) { - mKeyLetterRatio = UNDEFINED_RATIO; - mKeyLetterSize = a.getDimensionPixelSize(R.styleable.KeyboardView_keyLetterSize, 0); - } else { - mKeyLetterRatio = getRatio(a, R.styleable.KeyboardView_keyLetterRatio); - } - if (a.hasValue(R.styleable.KeyboardView_keyLabelSize)) { - mKeyLabelRatio = UNDEFINED_RATIO; - mKeyLabelSize = a.getDimensionPixelSize(R.styleable.KeyboardView_keyLabelSize, 0); - } else { - mKeyLabelRatio = getRatio(a, R.styleable.KeyboardView_keyLabelRatio); - } - mKeyLargeLabelRatio = getRatio(a, R.styleable.KeyboardView_keyLargeLabelRatio); - mKeyLargeLetterRatio = getRatio(a, R.styleable.KeyboardView_keyLargeLetterRatio); - mKeyHintLetterRatio = getRatio(a, R.styleable.KeyboardView_keyHintLetterRatio); - mKeyShiftedLetterHintRatio = getRatio(a, - R.styleable.KeyboardView_keyShiftedLetterHintRatio); - mKeyHintLabelRatio = getRatio(a, R.styleable.KeyboardView_keyHintLabelRatio); - mKeyLabelHorizontalPadding = a.getDimension( - R.styleable.KeyboardView_keyLabelHorizontalPadding, 0); - mKeyHintLetterPadding = a.getDimension( - R.styleable.KeyboardView_keyHintLetterPadding, 0); - mKeyPopupHintLetterPadding = a.getDimension( - R.styleable.KeyboardView_keyPopupHintLetterPadding, 0); - mKeyShiftedLetterHintPadding = a.getDimension( - R.styleable.KeyboardView_keyShiftedLetterHintPadding, 0); - mKeyTextColor = a.getColor(R.styleable.KeyboardView_keyTextColor, 0xFF000000); - mKeyTextInactivatedColor = a.getColor( - R.styleable.KeyboardView_keyTextInactivatedColor, 0xFF000000); - mKeyHintLetterColor = a.getColor(R.styleable.KeyboardView_keyHintLetterColor, 0); - mKeyHintLabelColor = a.getColor(R.styleable.KeyboardView_keyHintLabelColor, 0); - mKeyShiftedLetterHintInactivatedColor = a.getColor( - R.styleable.KeyboardView_keyShiftedLetterHintInactivatedColor, 0); - mKeyShiftedLetterHintActivatedColor = a.getColor( - R.styleable.KeyboardView_keyShiftedLetterHintActivatedColor, 0); - mKeyTextStyle = Typeface.defaultFromStyle( - a.getInt(R.styleable.KeyboardView_keyTextStyle, Typeface.NORMAL)); - mShadowColor = a.getColor(R.styleable.KeyboardView_shadowColor, 0); - mShadowRadius = a.getFloat(R.styleable.KeyboardView_shadowRadius, 0f); - - mKeyBackground.getPadding(mPadding); - } - - public void updateKeyHeight(int keyHeight) { - if (mKeyLetterRatio >= 0.0f) { - mKeyLetterSize = (int)(keyHeight * mKeyLetterRatio); - } - if (mKeyLabelRatio >= 0.0f) { - mKeyLabelSize = (int)(keyHeight * mKeyLabelRatio); - } - mKeyLargeLabelSize = (int)(keyHeight * mKeyLargeLabelRatio); - mKeyLargeLetterSize = (int)(keyHeight * mKeyLargeLetterRatio); - mKeyHintLetterSize = (int)(keyHeight * mKeyHintLetterRatio); - mKeyShiftedLetterHintSize = (int)(keyHeight * mKeyShiftedLetterHintRatio); - mKeyHintLabelSize = (int)(keyHeight * mKeyHintLabelRatio); - } - - public void blendAlpha(Paint paint) { - final int color = paint.getColor(); - paint.setARGB((paint.getAlpha() * mAnimAlpha) / Constants.Color.ALPHA_OPAQUE, - Color.red(color), Color.green(color), Color.blue(color)); - } - } - - /* package */ static class KeyPreviewDrawParams { - // XML attributes. - public final Drawable mPreviewBackground; - public final Drawable mPreviewLeftBackground; - public final Drawable mPreviewRightBackground; - public final int mPreviewTextColor; - public final int mPreviewOffset; - public final int mPreviewHeight; - public final Typeface mKeyTextStyle; - public final int mLingerTimeout; - - private final float mPreviewTextRatio; - private final float mKeyLetterRatio; - - // The graphical geometry of the key preview. - // <-width-> - // +-------+ ^ - // | | | - // |preview| height (visible) - // | | | - // + + ^ v - // \ / |offset - // +-\ /-+ v - // | +-+ | - // |parent | - // | key| - // +-------+ - // The background of a {@link TextView} being used for a key preview may have invisible - // paddings. To align the more keys keyboard panel's visible part with the visible part of - // the background, we need to record the width and height of key preview that don't include - // invisible paddings. - public int mPreviewVisibleWidth; - public int mPreviewVisibleHeight; - // The key preview may have an arbitrary offset and its background that may have a bottom - // padding. To align the more keys keyboard and the key preview we also need to record the - // offset between the top edge of parent key and the bottom of the visible part of key - // preview background. - public int mPreviewVisibleOffset; - - public int mPreviewTextSize; - public int mKeyLetterSize; - public final int[] mCoordinates = new int[2]; - - private static final int PREVIEW_ALPHA = 240; - - public KeyPreviewDrawParams(TypedArray a, KeyDrawParams keyDrawParams) { - mPreviewBackground = a.getDrawable(R.styleable.KeyboardView_keyPreviewBackground); - mPreviewLeftBackground = a.getDrawable( - R.styleable.KeyboardView_keyPreviewLeftBackground); - mPreviewRightBackground = a.getDrawable( - R.styleable.KeyboardView_keyPreviewRightBackground); - setAlpha(mPreviewBackground, PREVIEW_ALPHA); - setAlpha(mPreviewLeftBackground, PREVIEW_ALPHA); - setAlpha(mPreviewRightBackground, PREVIEW_ALPHA); - mPreviewOffset = a.getDimensionPixelOffset( - R.styleable.KeyboardView_keyPreviewOffset, 0); - mPreviewHeight = a.getDimensionPixelSize( - R.styleable.KeyboardView_keyPreviewHeight, 80); - mPreviewTextRatio = getRatio(a, R.styleable.KeyboardView_keyPreviewTextRatio); - mPreviewTextColor = a.getColor(R.styleable.KeyboardView_keyPreviewTextColor, 0); - mLingerTimeout = a.getInt(R.styleable.KeyboardView_keyPreviewLingerTimeout, 0); - - mKeyLetterRatio = keyDrawParams.mKeyLetterRatio; - mKeyTextStyle = keyDrawParams.mKeyTextStyle; - } - - public void updateKeyHeight(int keyHeight) { - if (mPreviewTextRatio >= 0.0f) { - mPreviewTextSize = (int)(keyHeight * mPreviewTextRatio); - } - if (mKeyLetterRatio >= 0.0f) { - mKeyLetterSize = (int)(keyHeight * mKeyLetterRatio); - } - } - - private static void setAlpha(Drawable drawable, int alpha) { - if (drawable == null) return; - drawable.setAlpha(alpha); - } - } - - public KeyboardView(Context context, AttributeSet attrs) { + public KeyboardView(final Context context, final AttributeSet attrs) { this(context, attrs, R.attr.keyboardViewStyle); } - public KeyboardView(Context context, AttributeSet attrs, int defStyle) { + public KeyboardView(final Context context, final AttributeSet attrs, final int defStyle) { super(context, attrs, defStyle); - final TypedArray a = context.obtainStyledAttributes( - attrs, R.styleable.KeyboardView, defStyle, R.style.KeyboardView); - - mKeyDrawParams = new KeyDrawParams(a); - mKeyPreviewDrawParams = new KeyPreviewDrawParams(a, mKeyDrawParams); - mKeyPreviewLayoutId = a.getResourceId(R.styleable.KeyboardView_keyPreviewLayout, 0); + final TypedArray keyboardViewAttr = context.obtainStyledAttributes(attrs, + R.styleable.KeyboardView, defStyle, R.style.KeyboardView); + mKeyBackground = keyboardViewAttr.getDrawable(R.styleable.KeyboardView_keyBackground); + mKeyBackground.getPadding(mKeyBackgroundPadding); + mPreviewBackground = keyboardViewAttr.getDrawable( + R.styleable.KeyboardView_keyPreviewBackground); + mPreviewLeftBackground = keyboardViewAttr.getDrawable( + R.styleable.KeyboardView_keyPreviewLeftBackground); + mPreviewRightBackground = keyboardViewAttr.getDrawable( + R.styleable.KeyboardView_keyPreviewRightBackground); + setAlpha(mPreviewBackground, PREVIEW_ALPHA); + setAlpha(mPreviewLeftBackground, PREVIEW_ALPHA); + setAlpha(mPreviewRightBackground, PREVIEW_ALPHA); + mPreviewOffset = keyboardViewAttr.getDimensionPixelOffset( + R.styleable.KeyboardView_keyPreviewOffset, 0); + mPreviewHeight = keyboardViewAttr.getDimensionPixelSize( + R.styleable.KeyboardView_keyPreviewHeight, 80); + mPreviewLingerTimeout = keyboardViewAttr.getInt( + R.styleable.KeyboardView_keyPreviewLingerTimeout, 0); + mDelayAfterPreview = mPreviewLingerTimeout; + mKeyLabelHorizontalPadding = keyboardViewAttr.getDimensionPixelOffset( + R.styleable.KeyboardView_keyLabelHorizontalPadding, 0); + mKeyHintLetterPadding = keyboardViewAttr.getDimension( + R.styleable.KeyboardView_keyHintLetterPadding, 0); + mKeyPopupHintLetterPadding = keyboardViewAttr.getDimension( + R.styleable.KeyboardView_keyPopupHintLetterPadding, 0); + mKeyShiftedLetterHintPadding = keyboardViewAttr.getDimension( + R.styleable.KeyboardView_keyShiftedLetterHintPadding, 0); + mKeyTextShadowRadius = keyboardViewAttr.getFloat( + R.styleable.KeyboardView_keyTextShadowRadius, 0.0f); + mKeyPreviewLayoutId = keyboardViewAttr.getResourceId( + R.styleable.KeyboardView_keyPreviewLayout, 0); if (mKeyPreviewLayoutId == 0) { mShowKeyPreviewPopup = false; } - mVerticalCorrection = a.getDimensionPixelOffset( + mVerticalCorrection = keyboardViewAttr.getDimension( R.styleable.KeyboardView_verticalCorrection, 0); - mMoreKeysLayout = a.getResourceId(R.styleable.KeyboardView_moreKeysLayout, 0); - mBackgroundDimAlpha = a.getInt(R.styleable.KeyboardView_backgroundDimAlpha, 0); - mPreviewPlacerView = new PreviewPlacerView(context, a); - a.recycle(); - - mDelayAfterPreview = mKeyPreviewDrawParams.mLingerTimeout; - + mMoreKeysLayout = keyboardViewAttr.getResourceId( + R.styleable.KeyboardView_moreKeysLayout, 0); + mBackgroundDimAlpha = keyboardViewAttr.getInt( + R.styleable.KeyboardView_backgroundDimAlpha, 0); + keyboardViewAttr.recycle(); + + final TypedArray keyAttr = context.obtainStyledAttributes(attrs, + R.styleable.Keyboard_Key, defStyle, R.style.KeyboardView); + mKeyVisualAttributes = KeyVisualAttributes.newInstance(keyAttr); + keyAttr.recycle(); + + mPreviewPlacerView = new PreviewPlacerView(context, attrs); mPaint.setAntiAlias(true); } - // Read fraction value in TypedArray as float. - /* package */ static float getRatio(TypedArray a, int index) { - return a.getFraction(index, 1000, 1000, 1) / 1000.0f; + private static void setAlpha(final Drawable drawable, final int alpha) { + if (drawable == null) return; + drawable.setAlpha(alpha); + } + + private static void blendAlpha(final Paint paint, final int alpha) { + final int color = paint.getColor(); + paint.setARGB((paint.getAlpha() * alpha) / Constants.Color.ALPHA_OPAQUE, + Color.red(color), Color.green(color), Color.blue(color)); } /** @@ -393,14 +296,14 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { * @see #getKeyboard() * @param keyboard the keyboard to display in this view */ - public void setKeyboard(Keyboard keyboard) { + public void setKeyboard(final Keyboard keyboard) { mKeyboard = keyboard; LatinImeLogger.onSetKeyboard(keyboard); requestLayout(); invalidateAllKeys(); final int keyHeight = keyboard.mMostCommonKeyHeight - keyboard.mVerticalGap; - mKeyDrawParams.updateKeyHeight(keyHeight); - mKeyPreviewDrawParams.updateKeyHeight(keyHeight); + mKeyDrawParams.updateParams(keyHeight, mKeyVisualAttributes); + mKeyDrawParams.updateParams(keyHeight, keyboard.mKeyVisualAttributes); } /** @@ -419,7 +322,7 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { * @param delay the delay after which the preview is dismissed * @see #isKeyPreviewPopupEnabled() */ - public void setKeyPreviewPopupEnabled(boolean previewEnabled, int delay) { + public void setKeyPreviewPopupEnabled(final boolean previewEnabled, final int delay) { mShowKeyPreviewPopup = previewEnabled; mDelayAfterPreview = delay; } @@ -433,14 +336,14 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { return mShowKeyPreviewPopup; } - public void setGesturePreviewMode(boolean drawsGesturePreviewTrail, - boolean drawsGestureFloatingPreviewText) { + public void setGesturePreviewMode(final boolean drawsGesturePreviewTrail, + final boolean drawsGestureFloatingPreviewText) { mPreviewPlacerView.setGesturePreviewMode( drawsGesturePreviewTrail, drawsGestureFloatingPreviewText); } @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { if (mKeyboard != null) { // The main keyboard expands to the display width. final int height = mKeyboard.mOccupiedHeight + getPaddingTop() + getPaddingBottom(); @@ -451,7 +354,7 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { } @Override - public void onDraw(Canvas canvas) { + public void onDraw(final Canvas canvas) { super.onDraw(canvas); if (canvas.isHardwareAccelerated()) { onDrawKeyboard(canvas); @@ -462,12 +365,7 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { if (bufferNeedsUpdates || mOffscreenBuffer == null) { if (maybeAllocateOffscreenBuffer()) { mInvalidateAllKeys = true; - // TODO: Stop using the offscreen canvas even when in software rendering - if (mOffscreenCanvas != null) { - mOffscreenCanvas.setBitmap(mOffscreenBuffer); - } else { - mOffscreenCanvas = new Canvas(mOffscreenBuffer); - } + maybeCreateOffscreenCanvas(); } onDrawKeyboard(mOffscreenCanvas); } @@ -496,13 +394,21 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { } } + private void maybeCreateOffscreenCanvas() { + // TODO: Stop using the offscreen canvas even when in software rendering + if (mOffscreenCanvas != null) { + mOffscreenCanvas.setBitmap(mOffscreenBuffer); + } else { + mOffscreenCanvas = new Canvas(mOffscreenBuffer); + } + } + private void onDrawKeyboard(final Canvas canvas) { if (mKeyboard == null) return; final int width = getWidth(); final int height = getHeight(); final Paint paint = mPaint; - final KeyDrawParams params = mKeyDrawParams; // Calculate clip region and set. final boolean drawAllKeys = mInvalidateAllKeys || mInvalidatedKeys.isEmpty(); @@ -535,13 +441,13 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { if (drawAllKeys || isHardwareAccelerated) { // Draw all keys. for (final Key key : mKeyboard.mKeys) { - onDrawKey(key, canvas, paint, params); + onDrawKey(key, canvas, paint); } } else { // Draw invalidated keys. for (final Key key : mInvalidatedKeys) { if (mKeyboard.hasKey(key)) { - onDrawKey(key, canvas, paint, params); + onDrawKey(key, canvas, paint); } } } @@ -565,7 +471,7 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { mInvalidateAllKeys = false; } - public void dimEntireKeyboard(boolean dimmed) { + public void dimEntireKeyboard(final boolean dimmed) { final boolean needsRedrawing = mNeedsToDimEntireKeyboard != dimmed; mNeedsToDimEntireKeyboard = dimmed; if (needsRedrawing) { @@ -573,14 +479,18 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { } } - private void onDrawKey(Key key, Canvas canvas, Paint paint, KeyDrawParams params) { - final int keyDrawX = key.mX + key.mVisualInsetsLeft + getPaddingLeft(); + private void onDrawKey(final Key key, final Canvas canvas, final Paint paint) { + final int keyDrawX = key.getDrawX() + getPaddingLeft(); final int keyDrawY = key.mY + getPaddingTop(); canvas.translate(keyDrawX, keyDrawY); + final int keyHeight = mKeyboard.mMostCommonKeyHeight - mKeyboard.mVerticalGap; + final KeyVisualAttributes attr = key.mKeyVisualAttributes; + final KeyDrawParams params = mKeyDrawParams.mayCloneAndUpdateParams(keyHeight, attr); params.mAnimAlpha = Constants.Color.ALPHA_OPAQUE; + if (!key.isSpacer()) { - onDrawKeyBackground(key, canvas, params); + onDrawKeyBackground(key, canvas); } onDrawKeyTopVisuals(key, canvas, paint, params); @@ -588,14 +498,14 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { } // Draw key background. - protected void onDrawKeyBackground(Key key, Canvas canvas, KeyDrawParams params) { - final int bgWidth = key.mWidth - key.mVisualInsetsLeft - key.mVisualInsetsRight - + params.mPadding.left + params.mPadding.right; - final int bgHeight = key.mHeight + params.mPadding.top + params.mPadding.bottom; - final int bgX = -params.mPadding.left; - final int bgY = -params.mPadding.top; + protected void onDrawKeyBackground(Key key, Canvas canvas) { + final Rect padding = mKeyBackgroundPadding; + final int bgWidth = key.getDrawWidth() + padding.left + padding.right; + final int bgHeight = key.mHeight + padding.top + padding.bottom; + final int bgX = -padding.left; + final int bgY = -padding.top; final int[] drawableState = key.getCurrentDrawableState(); - final Drawable background = params.mKeyBackground; + final Drawable background = mKeyBackground; background.setState(drawableState); final Rect bounds = background.getBounds(); if (bgWidth != bounds.right || bgHeight != bounds.bottom) { @@ -611,7 +521,7 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { // Draw key top visuals. protected void onDrawKeyTopVisuals(Key key, Canvas canvas, Paint paint, KeyDrawParams params) { - final int keyWidth = key.mWidth - key.mVisualInsetsLeft - key.mVisualInsetsRight; + final int keyWidth = key.getDrawWidth(); final int keyHeight = key.mHeight; final float centerX = keyWidth * 0.5f; final float centerY = keyHeight * 0.5f; @@ -625,12 +535,8 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { float positionX = centerX; if (key.mLabel != null) { final String label = key.mLabel; - // For characters, use large font. For labels like "Done", use smaller font. - paint.setTypeface(key.selectTypeface(params.mKeyTextStyle)); - final int labelSize = key.selectTextSize(params.mKeyLetterSize, - params.mKeyLargeLetterSize, params.mKeyLabelSize, params.mKeyLargeLabelSize, - params.mKeyHintLabelSize); - paint.setTextSize(labelSize); + paint.setTypeface(key.selectTypeface(params)); + paint.setTextSize(key.selectTextSize(params)); final float labelCharHeight = getCharHeight(KEY_LABEL_REFERENCE_CHAR, paint); final float labelCharWidth = getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint); @@ -640,10 +546,10 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { // Horizontal label text alignment float labelWidth = 0; if (key.isAlignLeft()) { - positionX = (int)params.mKeyLabelHorizontalPadding; + positionX = mKeyLabelHorizontalPadding; paint.setTextAlign(Align.LEFT); } else if (key.isAlignRight()) { - positionX = keyWidth - (int)params.mKeyLabelHorizontalPadding; + positionX = keyWidth - mKeyLabelHorizontalPadding; paint.setTextAlign(Align.RIGHT); } else if (key.isAlignLeftOfCenter()) { // TODO: Parameterise this? @@ -668,16 +574,15 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { Math.min(1.0f, (keyWidth * MAX_LABEL_RATIO) / getLabelWidth(label, paint))); } - paint.setColor(key.isShiftedLetterActivated() - ? params.mKeyTextInactivatedColor : params.mKeyTextColor); + paint.setColor(key.selectTextColor(params)); if (key.isEnabled()) { // Set a drop shadow for the text - paint.setShadowLayer(params.mShadowRadius, 0, 0, params.mShadowColor); + paint.setShadowLayer(mKeyTextShadowRadius, 0, 0, params.mTextShadowColor); } else { // Make label invisible paint.setColor(Color.TRANSPARENT); } - params.blendAlpha(paint); + blendAlpha(paint, params.mAnimAlpha); canvas.drawText(label, 0, label.length(), positionX, baseline, paint); // Turn off drop shadow and reset x-scale. paint.setShadowLayer(0, 0, 0, 0); @@ -705,25 +610,10 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { // Draw hint label. if (key.mHintLabel != null) { - final String hint = key.mHintLabel; - final int hintColor; - final int hintSize; - if (key.hasHintLabel()) { - hintColor = params.mKeyHintLabelColor; - hintSize = params.mKeyHintLabelSize; - paint.setTypeface(Typeface.DEFAULT); - } else if (key.hasShiftedLetterHint()) { - hintColor = key.isShiftedLetterActivated() - ? params.mKeyShiftedLetterHintActivatedColor - : params.mKeyShiftedLetterHintInactivatedColor; - hintSize = params.mKeyShiftedLetterHintSize; - } else { // key.hasHintLetter() - hintColor = params.mKeyHintLetterColor; - hintSize = params.mKeyHintLetterSize; - } - paint.setColor(hintColor); - params.blendAlpha(paint); - paint.setTextSize(hintSize); + final String hintLabel = key.mHintLabel; + paint.setTextSize(key.selectHintTextSize(params)); + paint.setColor(key.selectHintTextColor(params)); + blendAlpha(paint, params.mAnimAlpha); final float hintX, hintY; if (key.hasHintLabel()) { // The hint label is placed just right of the key label. Used mainly on @@ -734,19 +624,19 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { paint.setTextAlign(Align.LEFT); } else if (key.hasShiftedLetterHint()) { // The hint label is placed at top-right corner of the key. Used mainly on tablet. - hintX = keyWidth - params.mKeyShiftedLetterHintPadding + hintX = keyWidth - mKeyShiftedLetterHintPadding - getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) / 2; paint.getFontMetrics(mFontMetrics); hintY = -mFontMetrics.top; paint.setTextAlign(Align.CENTER); } else { // key.hasHintLetter() // The hint letter is placed at top-right corner of the key. Used mainly on phone. - hintX = keyWidth - params.mKeyHintLetterPadding + hintX = keyWidth - mKeyHintLetterPadding - getCharWidth(KEY_NUMERIC_HINT_LABEL_REFERENCE_CHAR, paint) / 2; hintY = -paint.ascent(); paint.setTextAlign(Align.CENTER); } - canvas.drawText(hint, 0, hint.length(), hintX, hintY, paint); + canvas.drawText(hintLabel, 0, hintLabel.length(), hintX, hintY, paint); if (LatinImeLogger.sVISUALDEBUG) { final Paint line = new Paint(); @@ -757,15 +647,15 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { // Draw key icon. if (key.mLabel == null && icon != null) { - final int iconWidth = icon.getIntrinsicWidth(); + final int iconWidth = Math.min(icon.getIntrinsicWidth(), keyWidth); final int iconHeight = icon.getIntrinsicHeight(); final int iconX, alignX; final int iconY = (keyHeight - iconHeight) / 2; if (key.isAlignLeft()) { - iconX = (int)params.mKeyLabelHorizontalPadding; + iconX = mKeyLabelHorizontalPadding; alignX = iconX; } else if (key.isAlignRight()) { - iconX = keyWidth - (int)params.mKeyLabelHorizontalPadding - iconWidth; + iconX = keyWidth - mKeyLabelHorizontalPadding - iconWidth; alignX = iconX + iconWidth; } else { // Align center iconX = (keyWidth - iconWidth) / 2; @@ -787,16 +677,16 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { // Draw popup hint "..." at the bottom right corner of the key. protected void drawKeyPopupHint(Key key, Canvas canvas, Paint paint, KeyDrawParams params) { - final int keyWidth = key.mWidth - key.mVisualInsetsLeft - key.mVisualInsetsRight; + final int keyWidth = key.getDrawWidth(); final int keyHeight = key.mHeight; - paint.setTypeface(params.mKeyTextStyle); - paint.setTextSize(params.mKeyHintLetterSize); - paint.setColor(params.mKeyHintLabelColor); + paint.setTypeface(params.mTypeface); + paint.setTextSize(params.mHintLetterSize); + paint.setColor(params.mHintLabelColor); paint.setTextAlign(Align.CENTER); - final float hintX = keyWidth - params.mKeyHintLetterPadding + final float hintX = keyWidth - mKeyHintLetterPadding - getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) / 2; - final float hintY = keyHeight - params.mKeyPopupHintLetterPadding; + final float hintY = keyHeight - mKeyPopupHintLetterPadding; canvas.drawText(POPUP_HINT_CHAR, hintX, hintY, paint); if (LatinImeLogger.sVISUALDEBUG) { @@ -889,8 +779,8 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { public Paint newDefaultLabelPaint() { final Paint paint = new Paint(); paint.setAntiAlias(true); - paint.setTypeface(mKeyDrawParams.mKeyTextStyle); - paint.setTextSize(mKeyDrawParams.mKeyLabelSize); + paint.setTypeface(mKeyDrawParams.mTypeface); + paint.setTextSize(mKeyDrawParams.mLabelSize); return paint; } @@ -901,23 +791,38 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { } } - // Called by {@link PointerTracker} constructor to create a TextView. - @Override - public TextView inflateKeyPreviewText() { + private TextView getKeyPreviewText(final int pointerId) { + TextView previewText = mKeyPreviewTexts.get(pointerId); + if (previewText != null) { + return previewText; + } final Context context = getContext(); if (mKeyPreviewLayoutId != 0) { - return (TextView)LayoutInflater.from(context).inflate(mKeyPreviewLayoutId, null); + previewText = (TextView)LayoutInflater.from(context).inflate(mKeyPreviewLayoutId, null); } else { - return new TextView(context); + previewText = new TextView(context); + } + mKeyPreviewTexts.put(pointerId, previewText); + return previewText; + } + + private void dismissAllKeyPreviews() { + final int pointerCount = mKeyPreviewTexts.size(); + for (int id = 0; id < pointerCount; id++) { + final TextView previewText = mKeyPreviewTexts.get(id); + if (previewText != null) { + previewText.setVisibility(INVISIBLE); + } } + PointerTracker.setReleasedKeyGraphicsToAllKeys(); } @Override - public void dismissKeyPreview(PointerTracker tracker) { + public void dismissKeyPreview(final PointerTracker tracker) { mDrawingHandler.dismissKeyPreview(mDelayAfterPreview, tracker); } - private void addKeyPreview(TextView keyPreview) { + private void addKeyPreview(final TextView keyPreview) { locatePreviewPlacerView(); mPreviewPlacerView.addView( keyPreview, ViewLayoutUtils.newLayoutParam(mPreviewPlacerView, 0, 0)); @@ -930,9 +835,18 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { final int[] viewOrigin = new int[2]; getLocationInWindow(viewOrigin); mPreviewPlacerView.setOrigin(viewOrigin[0], viewOrigin[1]); - final ViewGroup windowContentView = - (ViewGroup)getRootView().findViewById(android.R.id.content); - windowContentView.addView(mPreviewPlacerView); + final View rootView = getRootView(); + if (rootView == null) { + Log.w(TAG, "Cannot find root view"); + return; + } + final ViewGroup windowContentView = (ViewGroup)rootView.findViewById(android.R.id.content); + // Note: It'd be very weird if we get null by android.R.id.content. + if (windowContentView == null) { + Log.w(TAG, "Cannot find android.R.id.content view to add PreviewPlacerView"); + } else { + windowContentView.addView(mPreviewPlacerView); + } } public void showGestureFloatingPreviewText(String gestureFloatingPreviewText) { @@ -946,17 +860,21 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { } @Override - public void showGestureTrail(PointerTracker tracker) { + public void showGesturePreviewTrail(final PointerTracker tracker) { locatePreviewPlacerView(); mPreviewPlacerView.invalidatePointer(tracker); } @SuppressWarnings("deprecation") // setBackgroundDrawable is replaced by setBackground in API16 @Override - public void showKeyPreview(PointerTracker tracker) { - if (!mShowKeyPreviewPopup) return; + public void showKeyPreview(final PointerTracker tracker) { + final KeyPreviewDrawParams previewParams = mKeyPreviewDrawParams; + if (!mShowKeyPreviewPopup) { + previewParams.mPreviewVisibleOffset = -mKeyboard.mVerticalGap; + return; + } - final TextView previewText = tracker.getKeyPreviewText(); + final TextView previewText = getKeyPreviewText(tracker.mPointerId); // If the key preview has no parent view yet, add it to the ViewGroup which can place // key preview absolutely in SoftInputWindow. if (previewText.getParent() == null) { @@ -971,18 +889,18 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { if (key == null) return; - final KeyPreviewDrawParams params = mKeyPreviewDrawParams; + final KeyDrawParams drawParams = mKeyDrawParams; final String label = key.isShiftedLetterActivated() ? key.mHintLabel : key.mLabel; - // What we show as preview should match what we show on a key top in onBufferDraw(). + // What we show as preview should match what we show on a key top in onDraw(). if (label != null) { // TODO Should take care of temporaryShiftLabel here. previewText.setCompoundDrawables(null, null, null, null); if (StringUtils.codePointCount(label) > 1) { - previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, params.mKeyLetterSize); + previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, drawParams.mLetterSize); previewText.setTypeface(Typeface.DEFAULT_BOLD); } else { - previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, params.mPreviewTextSize); - previewText.setTypeface(params.mKeyTextStyle); + previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, drawParams.mPreviewTextSize); + previewText.setTypeface(key.selectTypeface(drawParams)); } previewText.setText(label); } else { @@ -990,48 +908,48 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { key.getPreviewIcon(mKeyboard.mIconsSet)); previewText.setText(null); } - previewText.setBackgroundDrawable(params.mPreviewBackground); + previewText.setBackgroundDrawable(mPreviewBackground); previewText.measure( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); - final int keyDrawWidth = key.mWidth - key.mVisualInsetsLeft - key.mVisualInsetsRight; + final int keyDrawWidth = key.getDrawWidth(); final int previewWidth = previewText.getMeasuredWidth(); - final int previewHeight = params.mPreviewHeight; + final int previewHeight = mPreviewHeight; // The width and height of visible part of the key preview background. The content marker // of the background 9-patch have to cover the visible part of the background. - params.mPreviewVisibleWidth = previewWidth - previewText.getPaddingLeft() + previewParams.mPreviewVisibleWidth = previewWidth - previewText.getPaddingLeft() - previewText.getPaddingRight(); - params.mPreviewVisibleHeight = previewHeight - previewText.getPaddingTop() + previewParams.mPreviewVisibleHeight = previewHeight - previewText.getPaddingTop() - previewText.getPaddingBottom(); // The distance between the top edge of the parent key and the bottom of the visible part // of the key preview background. - params.mPreviewVisibleOffset = params.mPreviewOffset - previewText.getPaddingBottom(); - getLocationInWindow(params.mCoordinates); + previewParams.mPreviewVisibleOffset = mPreviewOffset - previewText.getPaddingBottom(); + getLocationInWindow(previewParams.mCoordinates); // The key preview is horizontally aligned with the center of the visible part of the // parent key. If it doesn't fit in this {@link KeyboardView}, it is moved inward to fit and // the left/right background is used if such background is specified. - int previewX = key.mX + key.mVisualInsetsLeft - (previewWidth - keyDrawWidth) / 2 - + params.mCoordinates[0]; + int previewX = key.getDrawX() - (previewWidth - keyDrawWidth) / 2 + + previewParams.mCoordinates[0]; if (previewX < 0) { previewX = 0; - if (params.mPreviewLeftBackground != null) { - previewText.setBackgroundDrawable(params.mPreviewLeftBackground); + if (mPreviewLeftBackground != null) { + previewText.setBackgroundDrawable(mPreviewLeftBackground); } } else if (previewX > getWidth() - previewWidth) { previewX = getWidth() - previewWidth; - if (params.mPreviewRightBackground != null) { - previewText.setBackgroundDrawable(params.mPreviewRightBackground); + if (mPreviewRightBackground != null) { + previewText.setBackgroundDrawable(mPreviewRightBackground); } } // The key preview is placed vertically above the top edge of the parent key with an // arbitrary offset. - final int previewY = key.mY - previewHeight + params.mPreviewOffset - + params.mCoordinates[1]; + final int previewY = key.mY - previewHeight + mPreviewOffset + + previewParams.mCoordinates[1]; // Set the preview background state previewText.getBackground().setState( key.mMoreKeys != null ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET); - previewText.setTextColor(params.mPreviewTextColor); + previewText.setTextColor(drawParams.mPreviewTextColor); ViewLayoutUtils.placeViewAt( previewText, previewX, previewY, previewWidth, previewHeight); previewText.setVisibility(VISIBLE); @@ -1067,7 +985,7 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { } public void closing() { - PointerTracker.dismissAllKeyPreviews(); + dismissAllKeyPreviews(); cancelAllMessages(); mInvalidateAllKeys = true; diff --git a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java index 0a929f33f..f6b66a79e 100644 --- a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java +++ b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java @@ -43,15 +43,16 @@ import com.android.inputmethod.accessibility.AccessibilityUtils; import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy; import com.android.inputmethod.keyboard.PointerTracker.DrawingProxy; import com.android.inputmethod.keyboard.PointerTracker.TimerProxy; +import com.android.inputmethod.keyboard.internal.KeyDrawParams; import com.android.inputmethod.keyboard.internal.SuddenJumpingTouchEventHandler; import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.LatinIME; import com.android.inputmethod.latin.LatinImeLogger; import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.ResourceUtils; import com.android.inputmethod.latin.StaticInnerHandlerWrapper; import com.android.inputmethod.latin.StringUtils; import com.android.inputmethod.latin.SubtypeLocale; -import com.android.inputmethod.latin.Utils; import com.android.inputmethod.latin.Utils.UsabilityStudyLogUtils; import com.android.inputmethod.latin.define.ProductionFlag; import com.android.inputmethod.research.ResearchLogger; @@ -62,9 +63,25 @@ import java.util.WeakHashMap; /** * A view that is responsible for detecting key presses and touch movements. * - * @attr ref R.styleable#KeyboardView_keyHysteresisDistance - * @attr ref R.styleable#KeyboardView_verticalCorrection - * @attr ref R.styleable#KeyboardView_popupLayout + * @attr ref R.styleable#MainKeyboardView_autoCorrectionSpacebarLedEnabled + * @attr ref R.styleable#MainKeyboardView_autoCorrectionSpacebarLedIcon + * @attr ref R.styleable#MainKeyboardView_spacebarTextRatio + * @attr ref R.styleable#MainKeyboardView_spacebarTextColor + * @attr ref R.styleable#MainKeyboardView_spacebarTextShadowColor + * @attr ref R.styleable#MainKeyboardView_languageOnSpacebarFinalAlpha + * @attr ref R.styleable#MainKeyboardView_languageOnSpacebarFadeoutAnimator + * @attr ref R.styleable#MainKeyboardView_altCodeKeyWhileTypingFadeoutAnimator + * @attr ref R.styleable#MainKeyboardView_altCodeKeyWhileTypingFadeinAnimator + * @attr ref R.styleable#MainKeyboardView_keyHysteresisDistance + * @attr ref R.styleable#MainKeyboardView_touchNoiseThresholdTime + * @attr ref R.styleable#MainKeyboardView_touchNoiseThresholdDistance + * @attr ref R.styleable#MainKeyboardView_slidingKeyInputEnable + * @attr ref R.styleable#MainKeyboardView_keyRepeatStartTimeout + * @attr ref R.styleable#MainKeyboardView_keyRepeatInterval + * @attr ref R.styleable#MainKeyboardView_longPressKeyTimeout + * @attr ref R.styleable#MainKeyboardView_longPressShiftKeyTimeout + * @attr ref R.styleable#MainKeyboardView_ignoreAltCodeKeyTimeout + * @attr ref R.styleable#MainKeyboardView_showMoreKeysKeyboardAtTouchPoint */ public class MainKeyboardView extends KeyboardView implements PointerTracker.KeyEventHandler, SuddenJumpingTouchEventHandler.ProcessMotionEvent { @@ -110,7 +127,6 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key new WeakHashMap<Key, MoreKeysPanel>(); private final boolean mConfigShowMoreKeysKeyboardAtTouchedPoint; - private final PointerTrackerParams mPointerTrackerParams; private final SuddenJumpingTouchEventHandler mTouchScreenRegulator; protected KeyDetector mKeyDetector; @@ -127,15 +143,30 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key private static final int MSG_LONGPRESS_KEY = 2; private static final int MSG_DOUBLE_TAP = 3; - private final KeyTimerParams mParams; + private final int mKeyRepeatStartTimeout; + private final int mKeyRepeatInterval; + private final int mLongPressKeyTimeout; + private final int mLongPressShiftKeyTimeout; + private final int mIgnoreAltCodeKeyTimeout; - public KeyTimerHandler(MainKeyboardView outerInstance, KeyTimerParams params) { + public KeyTimerHandler(final MainKeyboardView outerInstance, + final TypedArray mainKeyboardViewAttr) { super(outerInstance); - mParams = params; + + mKeyRepeatStartTimeout = mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_keyRepeatStartTimeout, 0); + mKeyRepeatInterval = mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_keyRepeatInterval, 0); + mLongPressKeyTimeout = mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_longPressKeyTimeout, 0); + mLongPressShiftKeyTimeout = mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_longPressShiftKeyTimeout, 0); + mIgnoreAltCodeKeyTimeout = mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_ignoreAltCodeKeyTimeout, 0); } @Override - public void handleMessage(Message msg) { + public void handleMessage(final Message msg) { final MainKeyboardView keyboardView = getOuterInstance(); final PointerTracker tracker = (PointerTracker) msg.obj; switch (msg.what) { @@ -146,7 +177,7 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key final Key currentKey = tracker.getKey(); if (currentKey != null && currentKey.mCode == msg.arg1) { tracker.onRegisterKey(currentKey); - startKeyRepeatTimer(tracker, mParams.mKeyRepeatInterval); + startKeyRepeatTimer(tracker, mKeyRepeatInterval); } break; case MSG_LONGPRESS_KEY: @@ -159,15 +190,15 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key } } - private void startKeyRepeatTimer(PointerTracker tracker, long delay) { + private void startKeyRepeatTimer(final PointerTracker tracker, final long delay) { final Key key = tracker.getKey(); if (key == null) return; sendMessageDelayed(obtainMessage(MSG_REPEAT_KEY, key.mCode, 0, tracker), delay); } @Override - public void startKeyRepeatTimer(PointerTracker tracker) { - startKeyRepeatTimer(tracker, mParams.mKeyRepeatStartTimeout); + public void startKeyRepeatTimer(final PointerTracker tracker) { + startKeyRepeatTimer(tracker, mKeyRepeatStartTimeout); } public void cancelKeyRepeatTimer() { @@ -180,12 +211,12 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key } @Override - public void startLongPressTimer(int code) { + public void startLongPressTimer(final int code) { cancelLongPressTimer(); final int delay; switch (code) { case Keyboard.CODE_SHIFT: - delay = mParams.mLongPressShiftKeyTimeout; + delay = mLongPressShiftKeyTimeout; break; default: delay = 0; @@ -197,7 +228,7 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key } @Override - public void startLongPressTimer(PointerTracker tracker) { + public void startLongPressTimer(final PointerTracker tracker) { cancelLongPressTimer(); if (tracker == null) { return; @@ -206,15 +237,15 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key final int delay; switch (key.mCode) { case Keyboard.CODE_SHIFT: - delay = mParams.mLongPressShiftKeyTimeout; + delay = mLongPressShiftKeyTimeout; break; default: if (KeyboardSwitcher.getInstance().isInMomentarySwitchState()) { // We use longer timeout for sliding finger input started from the symbols // mode key. - delay = mParams.mLongPressKeyTimeout * 3; + delay = mLongPressKeyTimeout * 3; } else { - delay = mParams.mLongPressKeyTimeout; + delay = mLongPressKeyTimeout; } break; } @@ -251,7 +282,7 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key } @Override - public void startTypingStateTimer(Key typedKey) { + public void startTypingStateTimer(final Key typedKey) { if (typedKey.isModifier() || typedKey.altCodeWhileTyping()) { return; } @@ -268,7 +299,7 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key } sendMessageDelayed( - obtainMessage(MSG_TYPING_STATE_EXPIRED), mParams.mIgnoreAltCodeKeyTimeout); + obtainMessage(MSG_TYPING_STATE_EXPIRED), mIgnoreAltCodeKeyTimeout); if (isTyping) { return; } @@ -307,55 +338,11 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key } } - public static class PointerTrackerParams { - public final boolean mSlidingKeyInputEnabled; - public final int mTouchNoiseThresholdTime; - public final float mTouchNoiseThresholdDistance; - - public static final PointerTrackerParams DEFAULT = new PointerTrackerParams(); - - private PointerTrackerParams() { - mSlidingKeyInputEnabled = false; - mTouchNoiseThresholdTime =0; - mTouchNoiseThresholdDistance = 0; - } - - public PointerTrackerParams(TypedArray mainKeyboardViewAttr) { - mSlidingKeyInputEnabled = mainKeyboardViewAttr.getBoolean( - R.styleable.MainKeyboardView_slidingKeyInputEnable, false); - mTouchNoiseThresholdTime = mainKeyboardViewAttr.getInt( - R.styleable.MainKeyboardView_touchNoiseThresholdTime, 0); - mTouchNoiseThresholdDistance = mainKeyboardViewAttr.getDimension( - R.styleable.MainKeyboardView_touchNoiseThresholdDistance, 0); - } - } - - static class KeyTimerParams { - public final int mKeyRepeatStartTimeout; - public final int mKeyRepeatInterval; - public final int mLongPressKeyTimeout; - public final int mLongPressShiftKeyTimeout; - public final int mIgnoreAltCodeKeyTimeout; - - public KeyTimerParams(TypedArray mainKeyboardViewAttr) { - mKeyRepeatStartTimeout = mainKeyboardViewAttr.getInt( - R.styleable.MainKeyboardView_keyRepeatStartTimeout, 0); - mKeyRepeatInterval = mainKeyboardViewAttr.getInt( - R.styleable.MainKeyboardView_keyRepeatInterval, 0); - mLongPressKeyTimeout = mainKeyboardViewAttr.getInt( - R.styleable.MainKeyboardView_longPressKeyTimeout, 0); - mLongPressShiftKeyTimeout = mainKeyboardViewAttr.getInt( - R.styleable.MainKeyboardView_longPressShiftKeyTimeout, 0); - mIgnoreAltCodeKeyTimeout = mainKeyboardViewAttr.getInt( - R.styleable.MainKeyboardView_ignoreAltCodeKeyTimeout, 0); - } - } - - public MainKeyboardView(Context context, AttributeSet attrs) { + public MainKeyboardView(final Context context, final AttributeSet attrs) { this(context, attrs, R.attr.mainKeyboardViewStyle); } - public MainKeyboardView(Context context, AttributeSet attrs, int defStyle) { + public MainKeyboardView(final Context context, final AttributeSet attrs, final int defStyle) { super(context, attrs, defStyle); mTouchScreenRegulator = new SuddenJumpingTouchEventHandler(getContext(), this); @@ -364,7 +351,7 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key .hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT); final Resources res = getResources(); final boolean needsPhantomSuddenMoveEventHack = Boolean.parseBoolean( - Utils.getDeviceOverrideValue(res, + ResourceUtils.getDeviceOverrideValue(res, R.array.phantom_sudden_move_event_device_list, "false")); PointerTracker.init(mHasDistinctMultitouch, needsPhantomSuddenMoveEventHack); @@ -374,8 +361,8 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key R.styleable.MainKeyboardView_autoCorrectionSpacebarLedEnabled, false); mAutoCorrectionSpacebarLedIcon = a.getDrawable( R.styleable.MainKeyboardView_autoCorrectionSpacebarLedIcon); - mSpacebarTextRatio = a.getFraction(R.styleable.MainKeyboardView_spacebarTextRatio, - 1000, 1000, 1) / 1000.0f; + mSpacebarTextRatio = a.getFraction( + R.styleable.MainKeyboardView_spacebarTextRatio, 1, 1, 1.0f); mSpacebarTextColor = a.getColor(R.styleable.MainKeyboardView_spacebarTextColor, 0); mSpacebarTextShadowColor = a.getColor( R.styleable.MainKeyboardView_spacebarTextShadowColor, 0); @@ -389,19 +376,15 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key final int altCodeKeyWhileTypingFadeinAnimatorResId = a.getResourceId( R.styleable.MainKeyboardView_altCodeKeyWhileTypingFadeinAnimator, 0); - final KeyTimerParams keyTimerParams = new KeyTimerParams(a); - mPointerTrackerParams = new PointerTrackerParams(a); - final float keyHysteresisDistance = a.getDimension( R.styleable.MainKeyboardView_keyHysteresisDistance, 0); mKeyDetector = new KeyDetector(keyHysteresisDistance); - mKeyTimerHandler = new KeyTimerHandler(this, keyTimerParams); + mKeyTimerHandler = new KeyTimerHandler(this, a); mConfigShowMoreKeysKeyboardAtTouchedPoint = a.getBoolean( R.styleable.MainKeyboardView_showMoreKeysKeyboardAtTouchedPoint, false); + PointerTracker.setParameters(a); a.recycle(); - PointerTracker.setParameters(mPointerTrackerParams); - mLanguageOnSpacebarFadeoutAnimator = loadObjectAnimator( languageOnSpacebarFadeoutAnimatorResId, this); mAltCodeKeyWhileTypingFadeoutAnimator = loadObjectAnimator( @@ -410,7 +393,7 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key altCodeKeyWhileTypingFadeinAnimatorResId, this); } - private ObjectAnimator loadObjectAnimator(int resId, Object target) { + private ObjectAnimator loadObjectAnimator(final int resId, final Object target) { if (resId == 0) return null; final ObjectAnimator animator = (ObjectAnimator)AnimatorInflater.loadAnimator( getContext(), resId); @@ -425,7 +408,7 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key return mLanguageOnSpacebarAnimAlpha; } - public void setLanguageOnSpacebarAnimAlpha(int alpha) { + public void setLanguageOnSpacebarAnimAlpha(final int alpha) { mLanguageOnSpacebarAnimAlpha = alpha; invalidateKey(mSpaceKey); } @@ -434,12 +417,12 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key return mAltCodeKeyWhileTypingAnimAlpha; } - public void setAltCodeKeyWhileTypingAnimAlpha(int alpha) { + public void setAltCodeKeyWhileTypingAnimAlpha(final int alpha) { mAltCodeKeyWhileTypingAnimAlpha = alpha; updateAltCodeKeyWhileTyping(); } - public void setKeyboardActionListener(KeyboardActionListener listener) { + public void setKeyboardActionListener(final KeyboardActionListener listener) { mKeyboardActionListener = listener; PointerTracker.setKeyboardActionListener(listener); } @@ -476,7 +459,7 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key * @param keyboard the keyboard to display in this view */ @Override - public void setKeyboard(Keyboard keyboard) { + public void setKeyboard(final Keyboard keyboard) { // Remove any pending messages, except dismissing preview and key repeat. mKeyTimerHandler.cancelLongPressTimer(); super.setKeyboard(keyboard); @@ -501,11 +484,11 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key } // Note that this method is called from a non-UI thread. - public void setMainDictionaryAvailability(boolean mainDictionaryAvailable) { + public void setMainDictionaryAvailability(final boolean mainDictionaryAvailable) { PointerTracker.setMainDictionaryAvailability(mainDictionaryAvailable); } - public void setGestureHandlingEnabledByUser(boolean gestureHandlingEnabledByUser) { + public void setGestureHandlingEnabledByUser(final boolean gestureHandlingEnabledByUser) { PointerTracker.setGestureHandlingEnabledByUser(gestureHandlingEnabledByUser); } @@ -517,7 +500,7 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key return mHasDistinctMultitouch; } - public void setDistinctMultitouch(boolean hasDistinctMultitouch) { + public void setDistinctMultitouch(final boolean hasDistinctMultitouch) { mHasDistinctMultitouch = hasDistinctMultitouch; } @@ -528,7 +511,17 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key // to properly show the splash screen, which requires that the window token of the // KeyboardView be non-null. if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.getInstance().mainKeyboardView_onAttachedToWindow(); + ResearchLogger.getInstance().mainKeyboardView_onAttachedToWindow(this); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + // Notify the research logger that the keyboard view has been detached. This is needed + // to invalidate the reference of {@link MainKeyboardView} to null. + if (ProductionFlag.IS_EXPERIMENTAL) { + ResearchLogger.getInstance().mainKeyboardView_onDetachedFromWindow(); } } @@ -538,7 +531,8 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key super.cancelAllMessages(); } - private boolean openMoreKeysKeyboardIfRequired(Key parentKey, PointerTracker tracker) { + private boolean openMoreKeysKeyboardIfRequired(final Key parentKey, + final PointerTracker tracker) { // Check if we have a popup layout specified first. if (mMoreKeysLayout == 0) { return false; @@ -553,7 +547,7 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key } // This default implementation returns a more keys panel. - protected MoreKeysPanel onCreateMoreKeysPanel(Key parentKey) { + protected MoreKeysPanel onCreateMoreKeysPanel(final Key parentKey) { if (parentKey.mMoreKeys == null) return null; @@ -579,7 +573,7 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key * @return true if the long press is handled, false otherwise. Subclasses should call the * method on the base class if the subclass doesn't wish to handle the call. */ - protected boolean onLongPress(Key parentKey, PointerTracker tracker) { + protected boolean onLongPress(final Key parentKey, final PointerTracker tracker) { if (ProductionFlag.IS_EXPERIMENTAL) { ResearchLogger.mainKeyboardView_onLongPress(); } @@ -603,21 +597,20 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key return openMoreKeysPanel(parentKey, tracker); } - private boolean invokeCustomRequest(int code) { + private boolean invokeCustomRequest(final int code) { return mKeyboardActionListener.onCustomRequest(code); } - private void invokeCodeInput(int primaryCode) { - mKeyboardActionListener.onCodeInput(primaryCode, - KeyboardActionListener.NOT_A_TOUCH_COORDINATE, - KeyboardActionListener.NOT_A_TOUCH_COORDINATE); + private void invokeCodeInput(final int primaryCode) { + mKeyboardActionListener.onCodeInput( + primaryCode, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); } - private void invokeReleaseKey(int primaryCode) { + private void invokeReleaseKey(final int primaryCode) { mKeyboardActionListener.onReleaseKey(primaryCode, false); } - private boolean openMoreKeysPanel(Key parentKey, PointerTracker tracker) { + private boolean openMoreKeysPanel(final Key parentKey, final PointerTracker tracker) { MoreKeysPanel moreKeysPanel = mMoreKeysPanelCache.get(parentKey); if (moreKeysPanel == null) { moreKeysPanel = onCreateMoreKeysPanel(parentKey); @@ -643,9 +636,9 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key // The more keys keyboard is usually vertically aligned with the top edge of the parent key // (plus vertical gap). If the key preview is enabled, the more keys keyboard is vertically // aligned with the bottom edge of the visible part of the key preview. - final int pointY = parentKey.mY + (keyPreviewEnabled - ? mKeyPreviewDrawParams.mPreviewVisibleOffset - : -parentKey.mVerticalGap); + // {@code mPreviewVisibleOffset} has been set appropriately in + // {@link KeyboardView#showKeyPreview(PointerTracker)}. + final int pointY = parentKey.mY + mKeyPreviewDrawParams.mPreviewVisibleOffset; moreKeysPanel.showMoreKeysPanel( this, this, pointX, pointY, mMoreKeysWindow, mKeyboardActionListener); final int translatedX = moreKeysPanel.translateX(tracker.getLastX()); @@ -668,7 +661,7 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key } @Override - public boolean onTouchEvent(MotionEvent me) { + public boolean onTouchEvent(final MotionEvent me) { if (getKeyboard() == null) { return false; } @@ -679,7 +672,7 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key } @Override - public boolean processMotionEvent(MotionEvent me) { + public boolean processMotionEvent(final MotionEvent me) { final boolean nonDistinctMultitouch = !mHasDistinctMultitouch; final int action = me.getActionMasked(); final int pointerCount = me.getPointerCount(); @@ -846,7 +839,7 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key * otherwise */ @Override - public boolean dispatchHoverEvent(MotionEvent event) { + public boolean dispatchHoverEvent(final MotionEvent event) { if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) { final PointerTracker tracker = PointerTracker.getPointerTracker(0, this); return AccessibleKeyboardViewProxy.getInstance().dispatchHoverEvent(event, tracker); @@ -856,7 +849,7 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key return false; } - public void updateShortcutKey(boolean available) { + public void updateShortcutKey(final boolean available) { final Keyboard keyboard = getKeyboard(); if (keyboard == null) return; final Key shortcutKey = keyboard.getKey(Keyboard.CODE_SHORTCUT); @@ -873,8 +866,8 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key } } - public void startDisplayLanguageOnSpacebar(boolean subtypeChanged, - boolean needsToDisplayLanguage, boolean hasMultipleEnabledIMEsOrSubtypes) { + public void startDisplayLanguageOnSpacebar(final boolean subtypeChanged, + final boolean needsToDisplayLanguage, final boolean hasMultipleEnabledIMEsOrSubtypes) { mNeedsToDisplayLanguage = needsToDisplayLanguage; mHasMultipleEnabledIMEsOrSubtypes = hasMultipleEnabledIMEsOrSubtypes; final ObjectAnimator animator = mLanguageOnSpacebarFadeoutAnimator; @@ -896,14 +889,15 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key invalidateKey(mSpaceKey); } - public void updateAutoCorrectionState(boolean isAutoCorrection) { + public void updateAutoCorrectionState(final boolean isAutoCorrection) { if (!mAutoCorrectionSpacebarLedEnabled) return; mAutoCorrectionSpacebarLedOn = isAutoCorrection; invalidateKey(mSpaceKey); } @Override - protected void onDrawKeyTopVisuals(Key key, Canvas canvas, Paint paint, KeyDrawParams params) { + protected void onDrawKeyTopVisuals(final Key key, final Canvas canvas, final Paint paint, + final KeyDrawParams params) { if (key.altCodeWhileTyping() && key.isEnabled()) { params.mAnimAlpha = mAltCodeKeyWhileTypingAnimAlpha; } @@ -921,7 +915,7 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key } } - private boolean fitsTextIntoWidth(final int width, String text, Paint paint) { + private boolean fitsTextIntoWidth(final int width, final String text, final Paint paint) { paint.setTextScaleX(1.0f); final float textWidth = getLabelWidth(text, paint); if (textWidth < width) return true; @@ -934,7 +928,7 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key } // Layout language name on spacebar. - private String layoutLanguageOnSpacebar(Paint paint, InputMethodSubtype subtype, + private String layoutLanguageOnSpacebar(final Paint paint, final InputMethodSubtype subtype, final int width) { // Choose appropriate language name to fit into the width. String text = getFullDisplayName(subtype, getResources()); @@ -955,7 +949,7 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key return ""; } - private void drawSpacebar(Key key, Canvas canvas, Paint paint) { + private void drawSpacebar(final Key key, final Canvas canvas, final Paint paint) { final int width = key.mWidth; final int height = key.mHeight; @@ -1010,7 +1004,7 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key // zz azerty T AZERTY AZERTY // Get InputMethodSubtype's full display name in its locale. - static String getFullDisplayName(InputMethodSubtype subtype, Resources res) { + static String getFullDisplayName(final InputMethodSubtype subtype, final Resources res) { if (SubtypeLocale.isNoLanguage(subtype)) { return SubtypeLocale.getKeyboardLayoutSetDisplayName(subtype); } @@ -1019,7 +1013,7 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key } // Get InputMethodSubtype's short display name in its locale. - static String getShortDisplayName(InputMethodSubtype subtype) { + static String getShortDisplayName(final InputMethodSubtype subtype) { if (SubtypeLocale.isNoLanguage(subtype)) { return ""; } @@ -1028,7 +1022,7 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key } // Get InputMethodSubtype's middle display name in its locale. - static String getMiddleDisplayName(InputMethodSubtype subtype) { + static String getMiddleDisplayName(final InputMethodSubtype subtype) { if (SubtypeLocale.isNoLanguage(subtype)) { return SubtypeLocale.getKeyboardLayoutSetDisplayName(subtype); } diff --git a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java index a3741a2d8..c9af888f9 100644 --- a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java +++ b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java @@ -20,15 +20,17 @@ import android.graphics.Paint; import android.graphics.drawable.Drawable; import android.view.View; -import com.android.inputmethod.keyboard.internal.KeySpecParser.MoreKeySpec; +import com.android.inputmethod.keyboard.internal.KeyboardBuilder; import com.android.inputmethod.keyboard.internal.KeyboardIconsSet; +import com.android.inputmethod.keyboard.internal.KeyboardParams; +import com.android.inputmethod.keyboard.internal.MoreKeySpec; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.StringUtils; public class MoreKeysKeyboard extends Keyboard { private final int mDefaultKeyCoordX; - MoreKeysKeyboard(Builder.MoreKeysKeyboardParams params) { + MoreKeysKeyboard(final MoreKeysKeyboardParams params) { super(params); mDefaultKeyCoordX = params.getDefaultKeyCoordX() + params.mDefaultKeyWidth / 2; } @@ -37,228 +39,231 @@ public class MoreKeysKeyboard extends Keyboard { return mDefaultKeyCoordX; } - public static class Builder extends Keyboard.Builder<Builder.MoreKeysKeyboardParams> { - private final Key mParentKey; - private final Drawable mDivider; - - private static final float LABEL_PADDING_RATIO = 0.2f; - private static final float DIVIDER_RATIO = 0.2f; + /* package for test */ + static class MoreKeysKeyboardParams extends KeyboardParams { + public boolean mIsFixedOrder; + /* package */int mTopRowAdjustment; + public int mNumRows; + public int mNumColumns; + public int mTopKeys; + public int mLeftKeys; + public int mRightKeys; // includes default key. + public int mDividerWidth; + public int mColumnWidth; + + public MoreKeysKeyboardParams() { + super(); + } - public static class MoreKeysKeyboardParams extends Keyboard.Params { - public boolean mIsFixedOrder; - /* package */int mTopRowAdjustment; - public int mNumRows; - public int mNumColumns; - public int mTopKeys; - public int mLeftKeys; - public int mRightKeys; // includes default key. - public int mDividerWidth; - public int mColumnWidth; - - public MoreKeysKeyboardParams() { - super(); + /** + * Set keyboard parameters of more keys keyboard. + * + * @param numKeys number of keys in this more keys keyboard. + * @param maxColumns number of maximum columns of this more keys keyboard. + * @param keyWidth more keys keyboard key width in pixel, including horizontal gap. + * @param rowHeight more keys keyboard row height in pixel, including vertical gap. + * @param coordXInParent coordinate x of the key preview in parent keyboard. + * @param parentKeyboardWidth parent keyboard width in pixel. + * @param isFixedColumnOrder if true, more keys should be laid out in fixed order. + * @param dividerWidth width of divider, zero for no dividers. + */ + public void setParameters(final int numKeys, final int maxColumns, final int keyWidth, + final int rowHeight, final int coordXInParent, final int parentKeyboardWidth, + final boolean isFixedColumnOrder, final int dividerWidth) { + mIsFixedOrder = isFixedColumnOrder; + if (parentKeyboardWidth / keyWidth < maxColumns) { + throw new IllegalArgumentException( + "Keyboard is too small to hold more keys keyboard: " + + parentKeyboardWidth + " " + keyWidth + " " + maxColumns); } - - /** - * Set keyboard parameters of more keys keyboard. - * - * @param numKeys number of keys in this more keys keyboard. - * @param maxColumns number of maximum columns of this more keys keyboard. - * @param keyWidth more keys keyboard key width in pixel, including horizontal gap. - * @param rowHeight more keys keyboard row height in pixel, including vertical gap. - * @param coordXInParent coordinate x of the key preview in parent keyboard. - * @param parentKeyboardWidth parent keyboard width in pixel. - * @param isFixedColumnOrder if true, more keys should be laid out in fixed order. - * @param dividerWidth width of divider, zero for no dividers. - */ - public void setParameters(int numKeys, int maxColumns, int keyWidth, int rowHeight, - int coordXInParent, int parentKeyboardWidth, boolean isFixedColumnOrder, - int dividerWidth) { - mIsFixedOrder = isFixedColumnOrder; - if (parentKeyboardWidth / keyWidth < maxColumns) { - throw new IllegalArgumentException( - "Keyboard is too small to hold more keys keyboard: " - + parentKeyboardWidth + " " + keyWidth + " " + maxColumns); - } - mDefaultKeyWidth = keyWidth; - mDefaultRowHeight = rowHeight; - - final int numRows = (numKeys + maxColumns - 1) / maxColumns; - mNumRows = numRows; - final int numColumns = mIsFixedOrder ? Math.min(numKeys, maxColumns) - : getOptimizedColumns(numKeys, maxColumns); - mNumColumns = numColumns; - final int topKeys = numKeys % numColumns; - mTopKeys = topKeys == 0 ? numColumns : topKeys; - - final int numLeftKeys = (numColumns - 1) / 2; - final int numRightKeys = numColumns - numLeftKeys; // including default key. - // Maximum number of keys we can layout both side of the parent key - final int maxLeftKeys = coordXInParent / keyWidth; - final int maxRightKeys = (parentKeyboardWidth - coordXInParent) / keyWidth; - int leftKeys, rightKeys; - if (numLeftKeys > maxLeftKeys) { - leftKeys = maxLeftKeys; - rightKeys = numColumns - leftKeys; - } else if (numRightKeys > maxRightKeys + 1) { - rightKeys = maxRightKeys + 1; // include default key - leftKeys = numColumns - rightKeys; - } else { - leftKeys = numLeftKeys; - rightKeys = numRightKeys; - } - // If the left keys fill the left side of the parent key, entire more keys keyboard - // should be shifted to the right unless the parent key is on the left edge. - if (maxLeftKeys == leftKeys && leftKeys > 0) { - leftKeys--; - rightKeys++; - } - // If the right keys fill the right side of the parent key, entire more keys - // should be shifted to the left unless the parent key is on the right edge. - if (maxRightKeys == rightKeys - 1 && rightKeys > 1) { - leftKeys++; - rightKeys--; - } - mLeftKeys = leftKeys; - mRightKeys = rightKeys; - - // Adjustment of the top row. - mTopRowAdjustment = mIsFixedOrder ? getFixedOrderTopRowAdjustment() - : getAutoOrderTopRowAdjustment(); - mDividerWidth = dividerWidth; - mColumnWidth = mDefaultKeyWidth + mDividerWidth; - mBaseWidth = mOccupiedWidth = mNumColumns * mColumnWidth - mDividerWidth; - // Need to subtract the bottom row's gutter only. - mBaseHeight = mOccupiedHeight = mNumRows * mDefaultRowHeight - mVerticalGap - + mTopPadding + mBottomPadding; + mDefaultKeyWidth = keyWidth; + mDefaultRowHeight = rowHeight; + + final int numRows = (numKeys + maxColumns - 1) / maxColumns; + mNumRows = numRows; + final int numColumns = mIsFixedOrder ? Math.min(numKeys, maxColumns) + : getOptimizedColumns(numKeys, maxColumns); + mNumColumns = numColumns; + final int topKeys = numKeys % numColumns; + mTopKeys = topKeys == 0 ? numColumns : topKeys; + + final int numLeftKeys = (numColumns - 1) / 2; + final int numRightKeys = numColumns - numLeftKeys; // including default key. + // Maximum number of keys we can layout both side of the parent key + final int maxLeftKeys = coordXInParent / keyWidth; + final int maxRightKeys = (parentKeyboardWidth - coordXInParent) / keyWidth; + int leftKeys, rightKeys; + if (numLeftKeys > maxLeftKeys) { + leftKeys = maxLeftKeys; + rightKeys = numColumns - leftKeys; + } else if (numRightKeys > maxRightKeys + 1) { + rightKeys = maxRightKeys + 1; // include default key + leftKeys = numColumns - rightKeys; + } else { + leftKeys = numLeftKeys; + rightKeys = numRightKeys; } - - private int getFixedOrderTopRowAdjustment() { - if (mNumRows == 1 || mTopKeys % 2 == 1 || mTopKeys == mNumColumns - || mLeftKeys == 0 || mRightKeys == 1) { - return 0; - } - return -1; + // If the left keys fill the left side of the parent key, entire more keys keyboard + // should be shifted to the right unless the parent key is on the left edge. + if (maxLeftKeys == leftKeys && leftKeys > 0) { + leftKeys--; + rightKeys++; } - - private int getAutoOrderTopRowAdjustment() { - if (mNumRows == 1 || mTopKeys == 1 || mNumColumns % 2 == mTopKeys % 2 - || mLeftKeys == 0 || mRightKeys == 1) { - return 0; - } - return -1; + // If the right keys fill the right side of the parent key, entire more keys + // should be shifted to the left unless the parent key is on the right edge. + if (maxRightKeys == rightKeys - 1 && rightKeys > 1) { + leftKeys++; + rightKeys--; } + mLeftKeys = leftKeys; + mRightKeys = rightKeys; + + // Adjustment of the top row. + mTopRowAdjustment = mIsFixedOrder ? getFixedOrderTopRowAdjustment() + : getAutoOrderTopRowAdjustment(); + mDividerWidth = dividerWidth; + mColumnWidth = mDefaultKeyWidth + mDividerWidth; + mBaseWidth = mOccupiedWidth = mNumColumns * mColumnWidth - mDividerWidth; + // Need to subtract the bottom row's gutter only. + mBaseHeight = mOccupiedHeight = mNumRows * mDefaultRowHeight - mVerticalGap + + mTopPadding + mBottomPadding; + } - // Return key position according to column count (0 is default). - /* package */int getColumnPos(int n) { - return mIsFixedOrder ? getFixedOrderColumnPos(n) : getAutomaticColumnPos(n); + private int getFixedOrderTopRowAdjustment() { + if (mNumRows == 1 || mTopKeys % 2 == 1 || mTopKeys == mNumColumns + || mLeftKeys == 0 || mRightKeys == 1) { + return 0; } + return -1; + } - private int getFixedOrderColumnPos(int n) { - final int col = n % mNumColumns; - final int row = n / mNumColumns; - if (!isTopRow(row)) { - return col - mLeftKeys; - } - final int rightSideKeys = mTopKeys / 2; - final int leftSideKeys = mTopKeys - (rightSideKeys + 1); - final int pos = col - leftSideKeys; - final int numLeftKeys = mLeftKeys + mTopRowAdjustment; - final int numRightKeys = mRightKeys - 1; - if (numRightKeys >= rightSideKeys && numLeftKeys >= leftSideKeys) { - return pos; - } else if (numRightKeys < rightSideKeys) { - return pos - (rightSideKeys - numRightKeys); - } else { // numLeftKeys < leftSideKeys - return pos + (leftSideKeys - numLeftKeys); - } + private int getAutoOrderTopRowAdjustment() { + if (mNumRows == 1 || mTopKeys == 1 || mNumColumns % 2 == mTopKeys % 2 + || mLeftKeys == 0 || mRightKeys == 1) { + return 0; } + return -1; + } - private int getAutomaticColumnPos(int n) { - final int col = n % mNumColumns; - final int row = n / mNumColumns; - int leftKeys = mLeftKeys; - if (isTopRow(row)) { - leftKeys += mTopRowAdjustment; - } - if (col == 0) { - // default position. - return 0; - } + // Return key position according to column count (0 is default). + /* package */int getColumnPos(final int n) { + return mIsFixedOrder ? getFixedOrderColumnPos(n) : getAutomaticColumnPos(n); + } - int pos = 0; - int right = 1; // include default position key. - int left = 0; - int i = 0; - while (true) { - // Assign right key if available. - if (right < mRightKeys) { - pos = right; - right++; - i++; - } - if (i >= col) - break; - // Assign left key if available. - if (left < leftKeys) { - left++; - pos = -left; - i++; - } - if (i >= col) - break; - } + private int getFixedOrderColumnPos(final int n) { + final int col = n % mNumColumns; + final int row = n / mNumColumns; + if (!isTopRow(row)) { + return col - mLeftKeys; + } + final int rightSideKeys = mTopKeys / 2; + final int leftSideKeys = mTopKeys - (rightSideKeys + 1); + final int pos = col - leftSideKeys; + final int numLeftKeys = mLeftKeys + mTopRowAdjustment; + final int numRightKeys = mRightKeys - 1; + if (numRightKeys >= rightSideKeys && numLeftKeys >= leftSideKeys) { return pos; + } else if (numRightKeys < rightSideKeys) { + return pos - (rightSideKeys - numRightKeys); + } else { // numLeftKeys < leftSideKeys + return pos + (leftSideKeys - numLeftKeys); } + } - private static int getTopRowEmptySlots(int numKeys, int numColumns) { - final int remainings = numKeys % numColumns; - return remainings == 0 ? 0 : numColumns - remainings; + private int getAutomaticColumnPos(final int n) { + final int col = n % mNumColumns; + final int row = n / mNumColumns; + int leftKeys = mLeftKeys; + if (isTopRow(row)) { + leftKeys += mTopRowAdjustment; + } + if (col == 0) { + // default position. + return 0; } - private int getOptimizedColumns(int numKeys, int maxColumns) { - int numColumns = Math.min(numKeys, maxColumns); - while (getTopRowEmptySlots(numKeys, numColumns) >= mNumRows) { - numColumns--; + int pos = 0; + int right = 1; // include default position key. + int left = 0; + int i = 0; + while (true) { + // Assign right key if available. + if (right < mRightKeys) { + pos = right; + right++; + i++; } - return numColumns; + if (i >= col) + break; + // Assign left key if available. + if (left < leftKeys) { + left++; + pos = -left; + i++; + } + if (i >= col) + break; } + return pos; + } - public int getDefaultKeyCoordX() { - return mLeftKeys * mColumnWidth; - } + private static int getTopRowEmptySlots(final int numKeys, final int numColumns) { + final int remainings = numKeys % numColumns; + return remainings == 0 ? 0 : numColumns - remainings; + } - public int getX(int n, int row) { - final int x = getColumnPos(n) * mColumnWidth + getDefaultKeyCoordX(); - if (isTopRow(row)) { - return x + mTopRowAdjustment * (mColumnWidth / 2); - } - return x; + private int getOptimizedColumns(final int numKeys, final int maxColumns) { + int numColumns = Math.min(numKeys, maxColumns); + while (getTopRowEmptySlots(numKeys, numColumns) >= mNumRows) { + numColumns--; } + return numColumns; + } - public int getY(int row) { - return (mNumRows - 1 - row) * mDefaultRowHeight + mTopPadding; - } + public int getDefaultKeyCoordX() { + return mLeftKeys * mColumnWidth; + } - public void markAsEdgeKey(Key key, int row) { - if (row == 0) - key.markAsTopEdge(this); - if (isTopRow(row)) - key.markAsBottomEdge(this); + public int getX(final int n, final int row) { + final int x = getColumnPos(n) * mColumnWidth + getDefaultKeyCoordX(); + if (isTopRow(row)) { + return x + mTopRowAdjustment * (mColumnWidth / 2); } + return x; + } - private boolean isTopRow(int rowCount) { - return mNumRows > 1 && rowCount == mNumRows - 1; - } + public int getY(final int row) { + return (mNumRows - 1 - row) * mDefaultRowHeight + mTopPadding; } + public void markAsEdgeKey(final Key key, final int row) { + if (row == 0) + key.markAsTopEdge(this); + if (isTopRow(row)) + key.markAsBottomEdge(this); + } + + private boolean isTopRow(final int rowCount) { + return mNumRows > 1 && rowCount == mNumRows - 1; + } + } + + public static class Builder extends KeyboardBuilder<MoreKeysKeyboardParams> { + private final Key mParentKey; + private final Drawable mDivider; + + private static final float LABEL_PADDING_RATIO = 0.2f; + private static final float DIVIDER_RATIO = 0.2f; + + /** * The builder of MoreKeysKeyboard. * @param containerView the container of {@link MoreKeysKeyboardView}. * @param parentKey the {@link Key} that invokes more keys keyboard. * @param parentKeyboardView the {@link KeyboardView} that contains the parentKey. */ - public Builder(View containerView, Key parentKey, KeyboardView parentKeyboardView) { + public Builder(final View containerView, final Key parentKey, + final KeyboardView parentKeyboardView) { super(containerView.getContext(), new MoreKeysKeyboardParams()); final Keyboard parentKeyboard = parentKeyboardView.getKeyboard(); load(parentKeyboard.mMoreKeysTemplate, parentKeyboard.mId); @@ -300,14 +305,14 @@ public class MoreKeysKeyboard extends Keyboard { dividerWidth); } - private static int getMaxKeyWidth(KeyboardView view, Key parentKey, int minKeyWidth) { + private static int getMaxKeyWidth(final KeyboardView view, final Key parentKey, + final int minKeyWidth) { final int padding = (int)(view.getResources() .getDimension(R.dimen.more_keys_keyboard_key_horizontal_padding) + (parentKey.hasLabelsInMoreKeys() ? minKeyWidth * LABEL_PADDING_RATIO : 0)); final Paint paint = view.newDefaultLabelPaint(); - paint.setTextSize(parentKey.hasLabelsInMoreKeys() - ? view.mKeyDrawParams.mKeyLabelSize - : view.mKeyDrawParams.mKeyLetterSize); + paint.setTypeface(parentKey.selectTypeface(view.mKeyDrawParams)); + paint.setTextSize(parentKey.selectMoreKeyTextSize(view.mKeyDrawParams)); int maxWidth = minKeyWidth; for (final MoreKeySpec spec : parentKey.mMoreKeys) { final String label = spec.mLabel; @@ -322,24 +327,6 @@ public class MoreKeysKeyboard extends Keyboard { return maxWidth; } - private static class MoreKeyDivider extends Key.Spacer { - private final Drawable mIcon; - - public MoreKeyDivider(MoreKeysKeyboardParams params, Drawable icon, int x, int y) { - super(params, x, y, params.mDividerWidth, params.mDefaultRowHeight); - mIcon = icon; - } - - @Override - public Drawable getIcon(KeyboardIconsSet iconSet, int alpha) { - // KeyboardIconsSet and alpha are unused. Use the icon that has been passed to the - // constructor. - // TODO: Drawable itself should have an alpha value. - mIcon.setAlpha(128); - return mIcon; - } - } - @Override public MoreKeysKeyboard build() { final MoreKeysKeyboardParams params = mParams; @@ -368,4 +355,23 @@ public class MoreKeysKeyboard extends Keyboard { return new MoreKeysKeyboard(params); } } + + private static class MoreKeyDivider extends Key.Spacer { + private final Drawable mIcon; + + public MoreKeyDivider(final MoreKeysKeyboardParams params, final Drawable icon, + final int x, final int y) { + super(params, x, y, params.mDividerWidth, params.mDefaultRowHeight); + mIcon = icon; + } + + @Override + public Drawable getIcon(final KeyboardIconsSet iconSet, final int alpha) { + // KeyboardIconsSet and alpha are unused. Use the icon that has been passed to the + // constructor. + // TODO: Drawable itself should have an alpha value. + mIcon.setAlpha(128); + return mIcon; + } + } } diff --git a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java index 870eff29f..e513a1477 100644 --- a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java +++ b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java @@ -25,6 +25,7 @@ import android.widget.PopupWindow; import com.android.inputmethod.keyboard.PointerTracker.DrawingProxy; import com.android.inputmethod.keyboard.PointerTracker.TimerProxy; +import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.InputPointers; import com.android.inputmethod.latin.R; @@ -50,7 +51,8 @@ public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel public void onCodeInput(int primaryCode, int x, int y) { // Because a more keys keyboard doesn't need proximity characters correction, we don't // send touch event coordinates. - mListener.onCodeInput(primaryCode, NOT_A_TOUCH_COORDINATE, NOT_A_TOUCH_COORDINATE); + mListener.onCodeInput( + primaryCode, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); } @Override diff --git a/java/src/com/android/inputmethod/keyboard/PointerTracker.java b/java/src/com/android/inputmethod/keyboard/PointerTracker.java index 7d565a64f..5a79d508f 100644 --- a/java/src/com/android/inputmethod/keyboard/PointerTracker.java +++ b/java/src/com/android/inputmethod/keyboard/PointerTracker.java @@ -16,19 +16,19 @@ package com.android.inputmethod.keyboard; -import android.graphics.Canvas; -import android.graphics.Paint; +import android.content.res.TypedArray; import android.os.SystemClock; import android.util.Log; import android.view.MotionEvent; -import android.view.View; -import android.widget.TextView; import com.android.inputmethod.accessibility.AccessibilityUtils; import com.android.inputmethod.keyboard.internal.GestureStroke; +import com.android.inputmethod.keyboard.internal.GestureStrokeWithPreviewTrail; import com.android.inputmethod.keyboard.internal.PointerTrackerQueue; +import com.android.inputmethod.latin.CollectionUtils; import com.android.inputmethod.latin.InputPointers; import com.android.inputmethod.latin.LatinImeLogger; +import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.define.ProductionFlag; import com.android.inputmethod.research.ResearchLogger; @@ -78,10 +78,9 @@ public class PointerTracker implements PointerTrackerQueue.Element { public interface DrawingProxy extends MoreKeysPanel.Controller { public void invalidateKey(Key key); - public TextView inflateKeyPreviewText(); public void showKeyPreview(PointerTracker tracker); public void dismissKeyPreview(PointerTracker tracker); - public void showGestureTrail(PointerTracker tracker); + public void showGesturePreviewTrail(PointerTracker tracker); } public interface TimerProxy { @@ -120,14 +119,39 @@ public class PointerTracker implements PointerTrackerQueue.Element { } } + static class PointerTrackerParams { + public final boolean mSlidingKeyInputEnabled; + public final int mTouchNoiseThresholdTime; + public final float mTouchNoiseThresholdDistance; + public final int mTouchNoiseThresholdDistanceSquared; + + public static final PointerTrackerParams DEFAULT = new PointerTrackerParams(); + + private PointerTrackerParams() { + mSlidingKeyInputEnabled = false; + mTouchNoiseThresholdTime = 0; + mTouchNoiseThresholdDistance = 0.0f; + mTouchNoiseThresholdDistanceSquared = 0; + } + + public PointerTrackerParams(TypedArray mainKeyboardViewAttr) { + mSlidingKeyInputEnabled = mainKeyboardViewAttr.getBoolean( + R.styleable.MainKeyboardView_slidingKeyInputEnable, false); + mTouchNoiseThresholdTime = mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_touchNoiseThresholdTime, 0); + final float touchNouseThresholdDistance = mainKeyboardViewAttr.getDimension( + R.styleable.MainKeyboardView_touchNoiseThresholdDistance, 0); + mTouchNoiseThresholdDistance = touchNouseThresholdDistance; + mTouchNoiseThresholdDistanceSquared = + (int)(touchNouseThresholdDistance * touchNouseThresholdDistance); + } + } + // Parameters for pointer handling. - private static MainKeyboardView.PointerTrackerParams sParams; - private static int sTouchNoiseThresholdDistanceSquared; + private static PointerTrackerParams sParams; private static boolean sNeedsPhantomSuddenMoveEventHack; - private static final ArrayList<PointerTracker> sTrackers = new ArrayList<PointerTracker>(); - private static final InputPointers sAggregratedPointers = new InputPointers( - GestureStroke.DEFAULT_CAPACITY); + private static final ArrayList<PointerTracker> sTrackers = CollectionUtils.newArrayList(); private static PointerTrackerQueue sPointerTrackerQueue; public final int mPointerId; @@ -139,15 +163,14 @@ public class PointerTracker implements PointerTrackerQueue.Element { private Keyboard mKeyboard; private int mKeyQuarterWidthSquared; - private final TextView mKeyPreviewText; - private boolean mIsAlphabetKeyboard; - private boolean mIsPossibleGesture = false; - private boolean mInGesture = false; - - // TODO: Remove these variables - private int mLastRecognitionPointSize = 0; - private long mLastRecognitionTime = 0; + private boolean mIsDetectingGesture = false; // per PointerTracker. + private static boolean sInGesture = false; + private static long sGestureFirstDownTime; + private static final InputPointers sAggregratedPointers = new InputPointers( + GestureStroke.DEFAULT_CAPACITY); + private static int sLastRecognitionPointSize = 0; + private static long sLastRecognitionTime = 0; // The position and time at which first down event occurred. private long mDownTime; @@ -185,7 +208,7 @@ public class PointerTracker implements PointerTrackerQueue.Element { private static final KeyboardActionListener EMPTY_LISTENER = new KeyboardActionListener.Adapter(); - private final GestureStroke mGestureStroke; + private final GestureStrokeWithPreviewTrail mGestureStrokeWithPreviewTrail; public static void init(boolean hasDistinctMultitouch, boolean needsPhantomSuddenMoveEventHack) { @@ -195,14 +218,11 @@ public class PointerTracker implements PointerTrackerQueue.Element { sPointerTrackerQueue = null; } sNeedsPhantomSuddenMoveEventHack = needsPhantomSuddenMoveEventHack; - - setParameters(MainKeyboardView.PointerTrackerParams.DEFAULT); + sParams = PointerTrackerParams.DEFAULT; } - public static void setParameters(MainKeyboardView.PointerTrackerParams params) { - sParams = params; - sTouchNoiseThresholdDistanceSquared = (int)( - params.mTouchNoiseThresholdDistance * params.mTouchNoiseThresholdDistance); + public static void setParameters(final TypedArray mainKeyboardViewAttr) { + sParams = new PointerTrackerParams(mainKeyboardViewAttr); } private static void updateGestureHandlingMode() { @@ -213,17 +233,17 @@ public class PointerTracker implements PointerTrackerQueue.Element { } // Note that this method is called from a non-UI thread. - public static void setMainDictionaryAvailability(boolean mainDictionaryAvailable) { + public static void setMainDictionaryAvailability(final boolean mainDictionaryAvailable) { sMainDictionaryAvailable = mainDictionaryAvailable; updateGestureHandlingMode(); } - public static void setGestureHandlingEnabledByUser(boolean gestureHandlingEnabledByUser) { + public static void setGestureHandlingEnabledByUser(final boolean gestureHandlingEnabledByUser) { sGestureHandlingEnabledByUser = gestureHandlingEnabledByUser; updateGestureHandlingMode(); } - public static PointerTracker getPointerTracker(final int id, KeyEventHandler handler) { + public static PointerTracker getPointerTracker(final int id, final KeyEventHandler handler) { final ArrayList<PointerTracker> trackers = sTrackers; // Create pointer trackers until we can get 'id+1'-th tracker, if needed. @@ -239,7 +259,7 @@ public class PointerTracker implements PointerTrackerQueue.Element { return sPointerTrackerQueue != null ? sPointerTrackerQueue.isAnyInSlidingKeyInput() : false; } - public static void setKeyboardActionListener(KeyboardActionListener listener) { + public static void setKeyboardActionListener(final KeyboardActionListener listener) { final int trackersSize = sTrackers.size(); for (int i = 0; i < trackersSize; ++i) { final PointerTracker tracker = sTrackers.get(i); @@ -247,7 +267,7 @@ public class PointerTracker implements PointerTrackerQueue.Element { } } - public static void setKeyDetector(KeyDetector keyDetector) { + public static void setKeyDetector(final KeyDetector keyDetector) { final int trackersSize = sTrackers.size(); for (int i = 0; i < trackersSize; ++i) { final PointerTracker tracker = sTrackers.get(i); @@ -260,67 +280,29 @@ public class PointerTracker implements PointerTrackerQueue.Element { updateGestureHandlingMode(); } - public static void dismissAllKeyPreviews() { + public static void setReleasedKeyGraphicsToAllKeys() { final int trackersSize = sTrackers.size(); for (int i = 0; i < trackersSize; ++i) { final PointerTracker tracker = sTrackers.get(i); - tracker.getKeyPreviewText().setVisibility(View.INVISIBLE); tracker.setReleasedKeyGraphics(tracker.mCurrentKey); } } - // TODO: To handle multi-touch gestures we may want to move this method to - // {@link PointerTrackerQueue}. - private static InputPointers getIncrementalBatchPoints() { - final int trackersSize = sTrackers.size(); - for (int i = 0; i < trackersSize; ++i) { - final PointerTracker tracker = sTrackers.get(i); - tracker.mGestureStroke.appendIncrementalBatchPoints(sAggregratedPointers); - } - return sAggregratedPointers; - } - - // TODO: To handle multi-touch gestures we may want to move this method to - // {@link PointerTrackerQueue}. - private static InputPointers getAllBatchPoints() { - final int trackersSize = sTrackers.size(); - for (int i = 0; i < trackersSize; ++i) { - final PointerTracker tracker = sTrackers.get(i); - tracker.mGestureStroke.appendAllBatchPoints(sAggregratedPointers); - } - return sAggregratedPointers; - } - - // TODO: To handle multi-touch gestures we may want to move this method to - // {@link PointerTrackerQueue}. - public static void clearBatchInputPointsOfAllPointerTrackers() { - final int trackersSize = sTrackers.size(); - for (int i = 0; i < trackersSize; ++i) { - final PointerTracker tracker = sTrackers.get(i); - tracker.mGestureStroke.reset(); - } - sAggregratedPointers.reset(); - } - - private PointerTracker(int id, KeyEventHandler handler) { - if (handler == null) + private PointerTracker(final int id, final KeyEventHandler handler) { + if (handler == null) { throw new NullPointerException(); + } mPointerId = id; - mGestureStroke = new GestureStroke(id); + mGestureStrokeWithPreviewTrail = new GestureStrokeWithPreviewTrail(id); setKeyDetectorInner(handler.getKeyDetector()); mListener = handler.getKeyboardActionListener(); mDrawingProxy = handler.getDrawingProxy(); mTimerProxy = handler.getTimerProxy(); - mKeyPreviewText = mDrawingProxy.inflateKeyPreviewText(); - } - - public TextView getKeyPreviewText() { - return mKeyPreviewText; } // Returns true if keyboard has been changed by this callback. - private boolean callListenerOnPressAndCheckKeyboardLayoutChange(Key key) { - if (mInGesture) { + private boolean callListenerOnPressAndCheckKeyboardLayoutChange(final Key key) { + if (sInGesture) { return false; } final boolean ignoreModifierKey = mIgnoreModifierKey && key.isModifier(); @@ -344,13 +326,14 @@ public class PointerTracker implements PointerTrackerQueue.Element { // Note that we need primaryCode argument because the keyboard may in shifted state and the // primaryCode is different from {@link Key#mCode}. - private void callListenerOnCodeInput(Key key, int primaryCode, int x, int y) { + private void callListenerOnCodeInput(final Key key, final int primaryCode, final int x, + final int y) { final boolean ignoreModifierKey = mIgnoreModifierKey && key.isModifier(); final boolean altersCode = key.altCodeWhileTyping() && mTimerProxy.isTypingState(); - final int code = altersCode ? key.mAltCode : primaryCode; + final int code = altersCode ? key.getAltCode() : primaryCode; if (DEBUG_LISTENER) { - Log.d(TAG, "onCodeInput: " + Keyboard.printableCode(code) + " text=" + key.mOutputText - + " x=" + x + " y=" + y + Log.d(TAG, "onCodeInput: " + Keyboard.printableCode(code) + + " text=" + key.getOutputText() + " x=" + x + " y=" + y + " ignoreModifier=" + ignoreModifierKey + " altersCode=" + altersCode + " enabled=" + key.isEnabled()); } @@ -364,7 +347,7 @@ public class PointerTracker implements PointerTrackerQueue.Element { // Even if the key is disabled, it should respond if it is in the altCodeWhileTyping state. if (key.isEnabled() || altersCode) { if (code == Keyboard.CODE_OUTPUT_TEXT) { - mListener.onTextInput(key.mOutputText); + mListener.onTextInput(key.getOutputText()); } else if (code != Keyboard.CODE_UNSPECIFIED) { mListener.onCodeInput(code, x, y); } @@ -373,8 +356,9 @@ public class PointerTracker implements PointerTrackerQueue.Element { // Note that we need primaryCode argument because the keyboard may in shifted state and the // primaryCode is different from {@link Key#mCode}. - private void callListenerOnRelease(Key key, int primaryCode, boolean withSliding) { - if (mInGesture) { + private void callListenerOnRelease(final Key key, final int primaryCode, + final boolean withSliding) { + if (sInGesture) { return; } final boolean ignoreModifierKey = mIgnoreModifierKey && key.isModifier(); @@ -396,19 +380,19 @@ public class PointerTracker implements PointerTrackerQueue.Element { } private void callListenerOnCancelInput() { - if (DEBUG_LISTENER) + if (DEBUG_LISTENER) { Log.d(TAG, "onCancelInput"); + } if (ProductionFlag.IS_EXPERIMENTAL) { ResearchLogger.pointerTracker_callListenerOnCancelInput(); } mListener.onCancelInput(); } - private void setKeyDetectorInner(KeyDetector keyDetector) { + private void setKeyDetectorInner(final KeyDetector keyDetector) { mKeyDetector = keyDetector; mKeyboard = keyDetector.getKeyboard(); - mIsAlphabetKeyboard = mKeyboard.mId.isAlphabetKeyboard(); - mGestureStroke.setGestureSampleLength(mKeyboard.mMostCommonKeyWidth); + mGestureStrokeWithPreviewTrail.setGestureSampleLength(mKeyboard.mMostCommonKeyWidth); final Key newKey = mKeyDetector.detectHitKey(mKeyX, mKeyY); if (newKey != mCurrentKey) { if (mDrawingProxy != null) { @@ -434,11 +418,11 @@ public class PointerTracker implements PointerTrackerQueue.Element { return mCurrentKey != null && mCurrentKey.isModifier(); } - public Key getKeyOn(int x, int y) { + public Key getKeyOn(final int x, final int y) { return mKeyDetector.detectHitKey(x, y); } - private void setReleasedKeyGraphics(Key key) { + private void setReleasedKeyGraphics(final Key key) { mDrawingProxy.dismissKeyPreview(this); if (key == null) { return; @@ -456,20 +440,20 @@ public class PointerTracker implements PointerTrackerQueue.Element { } if (key.altCodeWhileTyping()) { - final int altCode = key.mAltCode; + final int altCode = key.getAltCode(); final Key altKey = mKeyboard.getKey(altCode); if (altKey != null) { updateReleaseKeyGraphics(altKey); } for (final Key k : mKeyboard.mAltCodeKeysWhileTyping) { - if (k != key && k.mAltCode == altCode) { + if (k != key && k.getAltCode() == altCode) { updateReleaseKeyGraphics(k); } } } } - private void setPressedKeyGraphics(Key key) { + private void setPressedKeyGraphics(final Key key) { if (key == null) { return; } @@ -481,7 +465,7 @@ public class PointerTracker implements PointerTrackerQueue.Element { return; } - if (!key.noKeyPreview() && !mInGesture) { + if (!key.noKeyPreview() && !sInGesture) { mDrawingProxy.showKeyPreview(this); } updatePressKeyGraphics(key); @@ -495,33 +479,31 @@ public class PointerTracker implements PointerTrackerQueue.Element { } if (key.altCodeWhileTyping() && mTimerProxy.isTypingState()) { - final int altCode = key.mAltCode; + final int altCode = key.getAltCode(); final Key altKey = mKeyboard.getKey(altCode); if (altKey != null) { updatePressKeyGraphics(altKey); } for (final Key k : mKeyboard.mAltCodeKeysWhileTyping) { - if (k != key && k.mAltCode == altCode) { + if (k != key && k.getAltCode() == altCode) { updatePressKeyGraphics(k); } } } } - private void updateReleaseKeyGraphics(Key key) { + private void updateReleaseKeyGraphics(final Key key) { key.onReleased(); mDrawingProxy.invalidateKey(key); } - private void updatePressKeyGraphics(Key key) { + private void updatePressKeyGraphics(final Key key) { key.onPressed(); mDrawingProxy.invalidateKey(key); } - public void drawGestureTrail(Canvas canvas, Paint paint) { - if (mInGesture) { - mGestureStroke.drawGestureTrail(canvas, paint); - } + public GestureStrokeWithPreviewTrail getGestureStrokeWithPreviewTrail() { + return mGestureStrokeWithPreviewTrail; } public int getLastX() { @@ -536,76 +518,91 @@ public class PointerTracker implements PointerTrackerQueue.Element { return mDownTime; } - private Key onDownKey(int x, int y, long eventTime) { + private Key onDownKey(final int x, final int y, final long eventTime) { mDownTime = eventTime; return onMoveToNewKey(onMoveKeyInternal(x, y), x, y); } - private Key onMoveKeyInternal(int x, int y) { + private Key onMoveKeyInternal(final int x, final int y) { mLastX = x; mLastY = y; return mKeyDetector.detectHitKey(x, y); } - private Key onMoveKey(int x, int y) { + private Key onMoveKey(final int x, final int y) { return onMoveKeyInternal(x, y); } - private Key onMoveToNewKey(Key newKey, int x, int y) { + private Key onMoveToNewKey(final Key newKey, final int x, final int y) { mCurrentKey = newKey; mKeyX = x; mKeyY = y; return newKey; } + private static int getActivePointerTrackerCount() { + return (sPointerTrackerQueue == null) ? 1 : sPointerTrackerQueue.size(); + } + private void startBatchInput() { if (DEBUG_LISTENER) { Log.d(TAG, "onStartBatchInput"); } - mInGesture = true; + sInGesture = true; mListener.onStartBatchInput(); - } - - private void updateBatchInput(InputPointers batchPoints) { - if (DEBUG_LISTENER) { - Log.d(TAG, "onUpdateBatchInput: batchPoints=" + batchPoints.getPointerSize()); + mDrawingProxy.showGesturePreviewTrail(this); + } + + private void updateBatchInput(final long eventTime) { + synchronized (sAggregratedPointers) { + mGestureStrokeWithPreviewTrail.appendIncrementalBatchPoints(sAggregratedPointers); + final int size = sAggregratedPointers.getPointerSize(); + if (size > sLastRecognitionPointSize + && eventTime > sLastRecognitionTime + MIN_GESTURE_RECOGNITION_TIME) { + sLastRecognitionPointSize = size; + sLastRecognitionTime = eventTime; + if (DEBUG_LISTENER) { + Log.d(TAG, "onUpdateBatchInput: batchPoints=" + size); + } + mListener.onUpdateBatchInput(sAggregratedPointers); + } } - mListener.onUpdateBatchInput(batchPoints); + mDrawingProxy.showGesturePreviewTrail(this); } - private void endBatchInput(InputPointers batchPoints) { - if (DEBUG_LISTENER) { - Log.d(TAG, "onEndBatchInput: batchPoints=" + batchPoints.getPointerSize()); + private void endBatchInput() { + synchronized (sAggregratedPointers) { + mGestureStrokeWithPreviewTrail.appendAllBatchPoints(sAggregratedPointers); + if (getActivePointerTrackerCount() == 1) { + if (DEBUG_LISTENER) { + Log.d(TAG, "onEndBatchInput: batchPoints=" + + sAggregratedPointers.getPointerSize()); + } + sInGesture = false; + mListener.onEndBatchInput(sAggregratedPointers); + clearBatchInputPointsOfAllPointerTrackers(); + } } - mListener.onEndBatchInput(batchPoints); - clearBatchInputRecognitionStateOfThisPointerTracker(); - clearBatchInputPointsOfAllPointerTrackers(); + mDrawingProxy.showGesturePreviewTrail(this); } - private void abortBatchInput() { - clearBatchInputRecognitionStateOfThisPointerTracker(); + private static void abortBatchInput() { clearBatchInputPointsOfAllPointerTrackers(); } - private void clearBatchInputRecognitionStateOfThisPointerTracker() { - mIsPossibleGesture = false; - mInGesture = false; - mLastRecognitionPointSize = 0; - mLastRecognitionTime = 0; - } - - private boolean updateBatchInputRecognitionState(long eventTime, int size) { - if (size > mLastRecognitionPointSize - && eventTime > mLastRecognitionTime + MIN_GESTURE_RECOGNITION_TIME) { - mLastRecognitionPointSize = size; - mLastRecognitionTime = eventTime; - return true; + private static void clearBatchInputPointsOfAllPointerTrackers() { + final int trackersSize = sTrackers.size(); + for (int i = 0; i < trackersSize; ++i) { + final PointerTracker tracker = sTrackers.get(i); + tracker.mGestureStrokeWithPreviewTrail.reset(); } - return false; + sAggregratedPointers.reset(); + sLastRecognitionPointSize = 0; + sLastRecognitionTime = 0; } - public void processMotionEvent(int action, int x, int y, long eventTime, - KeyEventHandler handler) { + public void processMotionEvent(final int action, final int x, final int y, final long eventTime, + final KeyEventHandler handler) { switch (action) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_POINTER_DOWN: @@ -624,9 +621,11 @@ public class PointerTracker implements PointerTrackerQueue.Element { } } - public void onDownEvent(int x, int y, long eventTime, KeyEventHandler handler) { - if (DEBUG_EVENT) + public void onDownEvent(final int x, final int y, final long eventTime, + final KeyEventHandler handler) { + if (DEBUG_EVENT) { printTouchEvent("onDownEvent:", x, y, eventTime); + } mDrawingProxy = handler.getDrawingProxy(); mTimerProxy = handler.getTimerProxy(); @@ -638,7 +637,7 @@ public class PointerTracker implements PointerTrackerQueue.Element { final int dx = x - mLastX; final int dy = y - mLastY; final int distanceSquared = (dx * dx + dy * dy); - if (distanceSquared < sTouchNoiseThresholdDistanceSquared) { + if (distanceSquared < sParams.mTouchNoiseThresholdDistanceSquared) { if (DEBUG_MODE) Log.w(TAG, "onDownEvent: ignore potential noise: time=" + deltaT + " distance=" + distanceSquared); @@ -650,8 +649,8 @@ public class PointerTracker implements PointerTrackerQueue.Element { } } - final PointerTrackerQueue queue = sPointerTrackerQueue; final Key key = getKeyOn(x, y); + final PointerTrackerQueue queue = sPointerTrackerQueue; if (queue != null) { if (key != null && key.isModifier()) { // Before processing a down event of modifier key, all pointers already being @@ -661,20 +660,30 @@ public class PointerTracker implements PointerTrackerQueue.Element { queue.add(this); } onDownEventInternal(x, y, eventTime); - if (queue != null && queue.size() == 1) { - mIsPossibleGesture = false; + if (!sShouldHandleGesture) { + return; + } + final int activePointerTrackerCount = getActivePointerTrackerCount(); + if (activePointerTrackerCount == 1) { + mIsDetectingGesture = false; // A gesture should start only from the letter key. - if (sShouldHandleGesture && mIsAlphabetKeyboard && !mIsShowingMoreKeysPanel - && key != null && Keyboard.isLetterCode(key.mCode)) { - mIsPossibleGesture = true; - // TODO: pointer times should be relative to first down even in entire batch input - // instead of resetting to 0 for each new down event. - mGestureStroke.addPoint(x, y, 0, false); + final boolean isAlphabetKeyboard = (mKeyboard != null) + && mKeyboard.mId.isAlphabetKeyboard(); + if (isAlphabetKeyboard && !mIsShowingMoreKeysPanel && key != null + && Keyboard.isLetterCode(key.mCode)) { + mIsDetectingGesture = true; + sGestureFirstDownTime = eventTime; + mGestureStrokeWithPreviewTrail.addPoint(x, y, 0, false /* isHistorical */); } + } else if (sInGesture && activePointerTrackerCount > 1) { + mIsDetectingGesture = true; + final int elapsedTimeFromFirstDown = (int)(eventTime - sGestureFirstDownTime); + mGestureStrokeWithPreviewTrail.addPoint(x, y, elapsedTimeFromFirstDown, + false /* isHistorical */); } } - private void onDownEventInternal(int x, int y, long eventTime) { + private void onDownEventInternal(final int x, final int y, final long eventTime) { Key key = onDownKey(x, y, eventTime); // Sliding key is allowed when 1) enabled by configuration, 2) this pointer starts sliding // from modifier key, or 3) this pointer's KeyDetector always allows sliding input. @@ -699,40 +708,38 @@ public class PointerTracker implements PointerTrackerQueue.Element { } } - private void startSlidingKeyInput(Key key) { + private void startSlidingKeyInput(final Key key) { if (!mIsInSlidingKeyInput) { mIgnoreModifierKey = key.isModifier(); } mIsInSlidingKeyInput = true; } - private void onGestureMoveEvent(PointerTracker tracker, int x, int y, long eventTime, - boolean isHistorical, Key key) { - final int gestureTime = (int)(eventTime - tracker.getDownTime()); - if (sShouldHandleGesture && mIsPossibleGesture) { - final GestureStroke stroke = mGestureStroke; + private void onGestureMoveEvent(final int x, final int y, final long eventTime, + final boolean isHistorical, final Key key) { + final int gestureTime = (int)(eventTime - sGestureFirstDownTime); + if (mIsDetectingGesture) { + final GestureStroke stroke = mGestureStrokeWithPreviewTrail; stroke.addPoint(x, y, gestureTime, isHistorical); - if (!mInGesture && stroke.isStartOfAGesture()) { + if (!sInGesture && stroke.isStartOfAGesture()) { startBatchInput(); } - } - if (key != null && mInGesture) { - final InputPointers batchPoints = getIncrementalBatchPoints(); - mDrawingProxy.showGestureTrail(this); - if (updateBatchInputRecognitionState(eventTime, batchPoints.getPointerSize())) { - updateBatchInput(batchPoints); + if (sInGesture && key != null) { + updateBatchInput(eventTime); } } } - public void onMoveEvent(int x, int y, long eventTime, MotionEvent me) { - if (DEBUG_MOVE_EVENT) + public void onMoveEvent(final int x, final int y, final long eventTime, final MotionEvent me) { + if (DEBUG_MOVE_EVENT) { printTouchEvent("onMoveEvent:", x, y, eventTime); - if (mKeyAlreadyProcessed) + } + if (mKeyAlreadyProcessed) { return; + } - if (me != null) { + if (sShouldHandleGesture && me != null) { // Add historical points to gesture path. final int pointerIndex = me.findPointerIndex(mPointerId); final int historicalSize = me.getHistorySize(); @@ -740,24 +747,31 @@ public class PointerTracker implements PointerTrackerQueue.Element { final int historicalX = (int)me.getHistoricalX(pointerIndex, h); final int historicalY = (int)me.getHistoricalY(pointerIndex, h); final long historicalTime = me.getHistoricalEventTime(h); - onGestureMoveEvent(this, historicalX, historicalY, historicalTime, + onGestureMoveEvent(historicalX, historicalY, historicalTime, true /* isHistorical */, null); } } + onMoveEventInternal(x, y, eventTime); + } + + private void onMoveEventInternal(final int x, final int y, final long eventTime) { final int lastX = mLastX; final int lastY = mLastY; final Key oldKey = mCurrentKey; Key key = onMoveKey(x, y); - // Register move event on gesture tracker. - onGestureMoveEvent(this, x, y, eventTime, false /* isHistorical */, key); - if (mInGesture) { - mIgnoreModifierKey = true; - mTimerProxy.cancelLongPressTimer(); - mIsInSlidingKeyInput = true; - mCurrentKey = null; - setReleasedKeyGraphics(oldKey); + if (sShouldHandleGesture) { + // Register move event on gesture tracker. + onGestureMoveEvent(x, y, eventTime, false /* isHistorical */, key); + if (sInGesture) { + mIgnoreModifierKey = true; + mTimerProxy.cancelLongPressTimer(); + mIsInSlidingKeyInput = true; + mCurrentKey = null; + setReleasedKeyGraphics(oldKey); + return; + } } if (key != null) { @@ -802,7 +816,7 @@ public class PointerTracker implements PointerTrackerQueue.Element { // TODO: Should find a way to balance gesture detection and this hack. if (sNeedsPhantomSuddenMoveEventHack && lastMoveSquared >= mKeyQuarterWidthSquared - && !mIsPossibleGesture) { + && !mIsDetectingGesture) { if (DEBUG_MODE) { Log.w(TAG, String.format("onMoveEvent:" + " phantom sudden move event is translated to " @@ -820,11 +834,11 @@ public class PointerTracker implements PointerTrackerQueue.Element { // touch panels when there are close multiple touches. // Caveat: When in chording input mode with a modifier key, we don't use // this hack. - if (me != null && me.getPointerCount() > 1 + if (getActivePointerTrackerCount() > 1 && sPointerTrackerQueue != null && !sPointerTrackerQueue.hasModifierKeyOlderThan(this)) { onUpEventInternal(); } - if (!mIsPossibleGesture) { + if (!mIsDetectingGesture) { mKeyAlreadyProcessed = true; } setReleasedKeyGraphics(oldKey); @@ -842,7 +856,7 @@ public class PointerTracker implements PointerTrackerQueue.Element { if (mIsAllowedSlidingKeyInput) { onMoveToNewKey(key, x, y); } else { - if (!mIsPossibleGesture) { + if (!mIsDetectingGesture) { mKeyAlreadyProcessed = true; } } @@ -850,13 +864,14 @@ public class PointerTracker implements PointerTrackerQueue.Element { } } - public void onUpEvent(int x, int y, long eventTime) { - if (DEBUG_EVENT) + public void onUpEvent(final int x, final int y, final long eventTime) { + if (DEBUG_EVENT) { printTouchEvent("onUpEvent :", x, y, eventTime); + } final PointerTrackerQueue queue = sPointerTrackerQueue; if (queue != null) { - if (!mInGesture) { + if (!sInGesture) { if (mCurrentKey != null && mCurrentKey.isModifier()) { // Before processing an up event of modifier key, all pointers already being // tracked should be released. @@ -865,18 +880,21 @@ public class PointerTracker implements PointerTrackerQueue.Element { queue.releaseAllPointersOlderThan(this, eventTime); } } - queue.remove(this); } onUpEventInternal(); + if (queue != null) { + queue.remove(this); + } } // Let this pointer tracker know that one of newer-than-this pointer trackers got an up event. // This pointer tracker needs to keep the key top graphics "pressed", but needs to get a // "virtual" up event. @Override - public void onPhantomUpEvent(long eventTime) { - if (DEBUG_EVENT) + public void onPhantomUpEvent(final long eventTime) { + if (DEBUG_EVENT) { printTouchEvent("onPhntEvent:", getLastX(), getLastY(), eventTime); + } onUpEventInternal(); mKeyAlreadyProcessed = true; } @@ -884,37 +902,35 @@ public class PointerTracker implements PointerTrackerQueue.Element { private void onUpEventInternal() { mTimerProxy.cancelKeyTimers(); mIsInSlidingKeyInput = false; - mIsPossibleGesture = false; + mIsDetectingGesture = false; + final Key currentKey = mCurrentKey; + mCurrentKey = null; // Release the last pressed key. - setReleasedKeyGraphics(mCurrentKey); + setReleasedKeyGraphics(currentKey); if (mIsShowingMoreKeysPanel) { mDrawingProxy.dismissMoreKeysPanel(); mIsShowingMoreKeysPanel = false; } - if (mInGesture) { - // Register up event on gesture tracker. - // TODO: Figure out how to deal with multiple fingers that are in gesture, sliding, - // and/or tapping mode? - endBatchInput(getAllBatchPoints()); - if (mCurrentKey != null) { - callListenerOnRelease(mCurrentKey, mCurrentKey.mCode, true); - mCurrentKey = null; + if (sInGesture) { + if (currentKey != null) { + callListenerOnRelease(currentKey, currentKey.mCode, true); } - mDrawingProxy.showGestureTrail(this); + endBatchInput(); return; } - // This event will be recognized as a regular code input. Clear unused batch points so they - // are not mistakenly included in the next batch event. + // This event will be recognized as a regular code input. Clear unused possible batch points + // so they are not mistakenly displayed as preview. clearBatchInputPointsOfAllPointerTrackers(); - if (mKeyAlreadyProcessed) + if (mKeyAlreadyProcessed) { return; - if (mCurrentKey != null && !mCurrentKey.isRepeatable()) { - detectAndSendKey(mCurrentKey, mKeyX, mKeyY); + } + if (currentKey != null && !currentKey.isRepeatable()) { + detectAndSendKey(currentKey, mKeyX, mKeyY); } } - public void onShowMoreKeysPanel(int x, int y, KeyEventHandler handler) { + public void onShowMoreKeysPanel(final int x, final int y, final KeyEventHandler handler) { abortBatchInput(); onLongPressed(); mIsShowingMoreKeysPanel = true; @@ -930,9 +946,10 @@ public class PointerTracker implements PointerTrackerQueue.Element { } } - public void onCancelEvent(int x, int y, long eventTime) { - if (DEBUG_EVENT) + public void onCancelEvent(final int x, final int y, final long eventTime) { + if (DEBUG_EVENT) { printTouchEvent("onCancelEvt:", x, y, eventTime); + } final PointerTrackerQueue queue = sPointerTrackerQueue; if (queue != null) { @@ -952,24 +969,25 @@ public class PointerTracker implements PointerTrackerQueue.Element { } } - private void startRepeatKey(Key key) { - if (key != null && key.isRepeatable() && !mInGesture) { + private void startRepeatKey(final Key key) { + if (key != null && key.isRepeatable() && !sInGesture) { onRegisterKey(key); mTimerProxy.startKeyRepeatTimer(this); } } - public void onRegisterKey(Key key) { + public void onRegisterKey(final Key key) { if (key != null) { detectAndSendKey(key, key.mX, key.mY); mTimerProxy.startTypingStateTimer(key); } } - private boolean isMajorEnoughMoveToBeOnNewKey(int x, int y, Key newKey) { - if (mKeyDetector == null) + private boolean isMajorEnoughMoveToBeOnNewKey(final int x, final int y, final Key newKey) { + if (mKeyDetector == null) { throw new NullPointerException("keyboard and/or key detector not set"); - Key curKey = mCurrentKey; + } + final Key curKey = mCurrentKey; if (newKey == curKey) { return false; } else if (curKey != null) { @@ -980,24 +998,25 @@ public class PointerTracker implements PointerTrackerQueue.Element { } } - private void startLongPressTimer(Key key) { - if (key != null && key.isLongPressEnabled() && !mInGesture) { + private void startLongPressTimer(final Key key) { + if (key != null && key.isLongPressEnabled() && !sInGesture) { mTimerProxy.startLongPressTimer(this); } } - private void detectAndSendKey(Key key, int x, int y) { + private void detectAndSendKey(final Key key, final int x, final int y) { if (key == null) { callListenerOnCancelInput(); return; } - int code = key.mCode; + final int code = key.mCode; callListenerOnCodeInput(key, code, x, y); callListenerOnRelease(key, code, false); } - private void printTouchEvent(String title, int x, int y, long eventTime) { + private void printTouchEvent(final String title, final int x, final int y, + final long eventTime) { final Key key = mKeyDetector.detectHitKey(x, y); final String code = KeyDetector.printableCode(key); Log.d(TAG, String.format("%s%s[%d] %4d %4d %5d %s", title, diff --git a/java/src/com/android/inputmethod/keyboard/ProximityInfo.java b/java/src/com/android/inputmethod/keyboard/ProximityInfo.java index ac0a56ba3..e1b082c16 100644 --- a/java/src/com/android/inputmethod/keyboard/ProximityInfo.java +++ b/java/src/com/android/inputmethod/keyboard/ProximityInfo.java @@ -19,7 +19,8 @@ package com.android.inputmethod.keyboard; import android.graphics.Rect; import android.text.TextUtils; -import com.android.inputmethod.keyboard.Keyboard.Params.TouchPositionCorrection; +import com.android.inputmethod.keyboard.internal.TouchPositionCorrection; +import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.JniUtils; import java.util.Arrays; @@ -47,9 +48,10 @@ public class ProximityInfo { private final Key[][] mGridNeighbors; private final String mLocaleStr; - ProximityInfo(String localeStr, int gridWidth, int gridHeight, int minWidth, int height, - int mostCommonKeyWidth, int mostCommonKeyHeight, final Key[] keys, - TouchPositionCorrection touchPositionCorrection) { + ProximityInfo(final String localeStr, final int gridWidth, final int gridHeight, + final int minWidth, final int height, final int mostCommonKeyWidth, + final int mostCommonKeyHeight, final Key[] keys, + final TouchPositionCorrection touchPositionCorrection) { if (TextUtils.isEmpty(localeStr)) { mLocaleStr = ""; } else { @@ -80,7 +82,7 @@ public class ProximityInfo { } public static ProximityInfo createSpellCheckerProximityInfo(final int[] proximity, - int rowSize, int gridWidth, int gridHeight) { + final int rowSize, final int gridWidth, final int gridHeight) { final ProximityInfo spellCheckerProximityInfo = createDummyProximityInfo(); spellCheckerProximityInfo.mNativeProximityInfo = spellCheckerProximityInfo.setProximityInfoNative("", @@ -111,7 +113,7 @@ public class ProximityInfo { final Key[] keys = mKeys; final TouchPositionCorrection touchPositionCorrection = mTouchPositionCorrection; final int[] proximityCharsArray = new int[mGridSize * MAX_PROXIMITY_CHARS_SIZE]; - Arrays.fill(proximityCharsArray, KeyDetector.NOT_A_CODE); + Arrays.fill(proximityCharsArray, Constants.NOT_A_CODE); for (int i = 0; i < mGridSize; ++i) { final int proximityCharsLength = gridNeighborKeys[i].length; for (int j = 0; j < proximityCharsLength; ++j) { @@ -234,7 +236,7 @@ public class ProximityInfo { dest[index++] = code; } if (index < destLength) { - dest[index] = KeyDetector.NOT_A_CODE; + dest[index] = Constants.NOT_A_CODE; } } diff --git a/java/src/com/android/inputmethod/keyboard/ViewLayoutUtils.java b/java/src/com/android/inputmethod/keyboard/ViewLayoutUtils.java index ee5047083..dc12fa468 100644 --- a/java/src/com/android/inputmethod/keyboard/ViewLayoutUtils.java +++ b/java/src/com/android/inputmethod/keyboard/ViewLayoutUtils.java @@ -22,7 +22,7 @@ import android.view.ViewGroup.MarginLayoutParams; import android.widget.FrameLayout; import android.widget.RelativeLayout; -public class ViewLayoutUtils { +public final class ViewLayoutUtils { private ViewLayoutUtils() { // This utility class is not publicly instantiable. } diff --git a/java/src/com/android/inputmethod/keyboard/internal/GesturePreviewTrail.java b/java/src/com/android/inputmethod/keyboard/internal/GesturePreviewTrail.java new file mode 100644 index 000000000..e814d8009 --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/internal/GesturePreviewTrail.java @@ -0,0 +1,166 @@ +/* + * 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.keyboard.internal; + +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.os.SystemClock; + +import com.android.inputmethod.latin.Constants; +import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.ResizableIntArray; + +class GesturePreviewTrail { + private static final int DEFAULT_CAPACITY = GestureStrokeWithPreviewTrail.PREVIEW_CAPACITY; + + private final GesturePreviewTrailParams mPreviewParams; + private final ResizableIntArray mXCoordinates = new ResizableIntArray(DEFAULT_CAPACITY); + private final ResizableIntArray mYCoordinates = new ResizableIntArray(DEFAULT_CAPACITY); + private final ResizableIntArray mEventTimes = new ResizableIntArray(DEFAULT_CAPACITY); + private int mCurrentStrokeId = -1; + private long mCurrentDownTime; + private int mTrailStartIndex; + + // Use this value as imaginary zero because x-coordinates may be zero. + private static final int DOWN_EVENT_MARKER = -128; + + static class GesturePreviewTrailParams { + public final int mFadeoutStartDelay; + public final int mFadeoutDuration; + public final int mUpdateInterval; + + public GesturePreviewTrailParams(final TypedArray keyboardViewAttr) { + mFadeoutStartDelay = keyboardViewAttr.getInt( + R.styleable.KeyboardView_gesturePreviewTrailFadeoutStartDelay, 0); + mFadeoutDuration = keyboardViewAttr.getInt( + R.styleable.KeyboardView_gesturePreviewTrailFadeoutDuration, 0); + mUpdateInterval = keyboardViewAttr.getInt( + R.styleable.KeyboardView_gesturePreviewTrailUpdateInterval, 0); + } + } + + public GesturePreviewTrail(final GesturePreviewTrailParams params) { + mPreviewParams = params; + } + + private static int markAsDownEvent(final int xCoord) { + return DOWN_EVENT_MARKER - xCoord; + } + + private static boolean isDownEventXCoord(final int xCoordOrMark) { + return xCoordOrMark <= DOWN_EVENT_MARKER; + } + + private static int getXCoordValue(final int xCoordOrMark) { + return isDownEventXCoord(xCoordOrMark) + ? DOWN_EVENT_MARKER - xCoordOrMark : xCoordOrMark; + } + + public void addStroke(final GestureStrokeWithPreviewTrail stroke, final long downTime) { + final int strokeId = stroke.getGestureStrokeId(); + final boolean isNewStroke = strokeId != mCurrentStrokeId; + final int trailSize = mEventTimes.getLength(); + stroke.appendPreviewStroke(mEventTimes, mXCoordinates, mYCoordinates); + final int newTrailSize = mEventTimes.getLength(); + if (stroke.getGestureStrokePreviewSize() == 0) { + return; + } + if (isNewStroke) { + final int elapsedTime = (int)(downTime - mCurrentDownTime); + final int[] eventTimes = mEventTimes.getPrimitiveArray(); + for (int i = mTrailStartIndex; i < trailSize; i++) { + eventTimes[i] -= elapsedTime; + } + + if (newTrailSize > trailSize) { + final int[] xCoords = mXCoordinates.getPrimitiveArray(); + xCoords[trailSize] = markAsDownEvent(xCoords[trailSize]); + } + mCurrentDownTime = downTime; + mCurrentStrokeId = strokeId; + } + } + + private int getAlpha(final int elapsedTime) { + if (elapsedTime < mPreviewParams.mFadeoutStartDelay) { + return Constants.Color.ALPHA_OPAQUE; + } + final int decreasingAlpha = Constants.Color.ALPHA_OPAQUE + * (elapsedTime - mPreviewParams.mFadeoutStartDelay) + / mPreviewParams.mFadeoutDuration; + return Constants.Color.ALPHA_OPAQUE - decreasingAlpha; + } + + /** + * Draw gesture preview trail + * @param canvas The canvas to draw the gesture preview trail + * @param paint The paint object to be used to draw the gesture preview trail + * @return true if some gesture preview trails remain to be drawn + */ + public boolean drawGestureTrail(final Canvas canvas, final Paint paint) { + final int trailSize = mEventTimes.getLength(); + if (trailSize == 0) { + return false; + } + + final int[] eventTimes = mEventTimes.getPrimitiveArray(); + final int[] xCoords = mXCoordinates.getPrimitiveArray(); + final int[] yCoords = mYCoordinates.getPrimitiveArray(); + final int sinceDown = (int)(SystemClock.uptimeMillis() - mCurrentDownTime); + final int lingeringDuration = mPreviewParams.mFadeoutStartDelay + + mPreviewParams.mFadeoutDuration; + int startIndex; + for (startIndex = mTrailStartIndex; startIndex < trailSize; startIndex++) { + final int elapsedTime = sinceDown - eventTimes[startIndex]; + // Skip too old trail points. + if (elapsedTime < lingeringDuration) { + break; + } + } + mTrailStartIndex = startIndex; + + if (startIndex < trailSize) { + int lastX = getXCoordValue(xCoords[startIndex]); + int lastY = yCoords[startIndex]; + for (int i = startIndex + 1; i < trailSize - 1; i++) { + final int x = xCoords[i]; + final int y = yCoords[i]; + final int elapsedTime = sinceDown - eventTimes[i]; + // Draw trail line only when the current point isn't a down point. + if (!isDownEventXCoord(x)) { + paint.setAlpha(getAlpha(elapsedTime)); + canvas.drawLine(lastX, lastY, x, y, paint); + } + lastX = getXCoordValue(x); + lastY = y; + } + } + + final int newSize = trailSize - startIndex; + if (newSize < startIndex) { + mTrailStartIndex = 0; + if (newSize > 0) { + System.arraycopy(eventTimes, startIndex, eventTimes, 0, newSize); + System.arraycopy(xCoords, startIndex, xCoords, 0, newSize); + System.arraycopy(yCoords, startIndex, yCoords, 0, newSize); + } + mEventTimes.setLength(newSize); + mXCoordinates.setLength(newSize); + mYCoordinates.setLength(newSize); + } + return newSize > 0; + } +} diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureStroke.java b/java/src/com/android/inputmethod/keyboard/internal/GestureStroke.java index 79e977a40..825134468 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/GestureStroke.java +++ b/java/src/com/android/inputmethod/keyboard/internal/GestureStroke.java @@ -14,10 +14,6 @@ package com.android.inputmethod.keyboard.internal; -import android.graphics.Canvas; -import android.graphics.Paint; - -import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.InputPointers; import com.android.inputmethod.latin.ResizableIntArray; @@ -48,13 +44,8 @@ public class GestureStroke { private static final float DOUBLE_PI = (float)(2.0f * Math.PI); - // Fade based on number of gesture samples, see MIN_GESTURE_SAMPLING_RATIO_TO_KEY_HEIGHT - private static final int DRAWING_GESTURE_FADE_START = 10; - private static final int DRAWING_GESTURE_FADE_RATE = 6; - - public GestureStroke(int pointerId) { + public GestureStroke(final int pointerId) { mPointerId = pointerId; - reset(); } public void setGestureSampleLength(final int keyWidth) { @@ -139,8 +130,12 @@ public class GestureStroke { } private void appendBatchPoints(final InputPointers out, final int size) { + final int length = size - mLastIncrementalBatchSize; + if (length <= 0) { + return; + } out.append(mPointerId, mEventTimes, mXCoordinates, mYCoordinates, - mLastIncrementalBatchSize, size - mLastIncrementalBatchSize); + mLastIncrementalBatchSize, length); mLastIncrementalBatchSize = size; } @@ -158,7 +153,7 @@ public class GestureStroke { if (dx == 0 && dy == 0) return 0; // Would it be faster to call atan2f() directly via JNI? Not sure about what the JIT // does with Math.atan2(). - return (float)Math.atan2((double)dy, (double)dx); + return (float)Math.atan2(dy, dx); } private static float getAngleDiff(final float a1, final float a2) { @@ -168,20 +163,4 @@ public class GestureStroke { } return diff; } - - public void drawGestureTrail(final Canvas canvas, final Paint paint) { - // TODO: These paint parameter interpolation should be tunable, possibly introduce an object - // that implements an interface such as Paint getPaint(int step, int strokePoints) - final int size = mXCoordinates.getLength(); - final int[] xCoords = mXCoordinates.getPrimitiveArray(); - final int[] yCoords = mYCoordinates.getPrimitiveArray(); - int alpha = Constants.Color.ALPHA_OPAQUE; - for (int i = size - 1; i > 0 && alpha > 0; i--) { - paint.setAlpha(alpha); - if (size - i > DRAWING_GESTURE_FADE_START) { - alpha -= DRAWING_GESTURE_FADE_RATE; - } - canvas.drawLine(xCoords[i - 1], yCoords[i - 1], xCoords[i], yCoords[i], paint); - } - } } diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeWithPreviewTrail.java b/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeWithPreviewTrail.java new file mode 100644 index 000000000..6c1a9bc01 --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeWithPreviewTrail.java @@ -0,0 +1,70 @@ +/* + * 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.keyboard.internal; + +import com.android.inputmethod.latin.ResizableIntArray; + +public class GestureStrokeWithPreviewTrail extends GestureStroke { + public static final int PREVIEW_CAPACITY = 256; + + private final ResizableIntArray mPreviewEventTimes = new ResizableIntArray(PREVIEW_CAPACITY); + private final ResizableIntArray mPreviewXCoordinates = new ResizableIntArray(PREVIEW_CAPACITY); + private final ResizableIntArray mPreviewYCoordinates = new ResizableIntArray(PREVIEW_CAPACITY); + + private int mStrokeId; + private int mLastPreviewSize; + + public GestureStrokeWithPreviewTrail(final int pointerId) { + super(pointerId); + } + + @Override + public void reset() { + super.reset(); + mStrokeId++; + mLastPreviewSize = 0; + mPreviewEventTimes.setLength(0); + mPreviewXCoordinates.setLength(0); + mPreviewYCoordinates.setLength(0); + } + + public int getGestureStrokeId() { + return mStrokeId; + } + + public int getGestureStrokePreviewSize() { + return mPreviewEventTimes.getLength(); + } + + @Override + public void addPoint(final int x, final int y, final int time, final boolean isHistorical) { + super.addPoint(x, y, time, isHistorical); + mPreviewEventTimes.add(time); + mPreviewXCoordinates.add(x); + mPreviewYCoordinates.add(y); + } + + public void appendPreviewStroke(final ResizableIntArray eventTimes, + final ResizableIntArray xCoords, final ResizableIntArray yCoords) { + final int length = mPreviewEventTimes.getLength() - mLastPreviewSize; + if (length <= 0) { + return; + } + eventTimes.append(mPreviewEventTimes, mLastPreviewSize, length); + xCoords.append(mPreviewXCoordinates, mLastPreviewSize, length); + yCoords.append(mPreviewYCoordinates, mLastPreviewSize, length); + mLastPreviewSize = mPreviewEventTimes.getLength(); + } +} diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyDrawParams.java b/java/src/com/android/inputmethod/keyboard/internal/KeyDrawParams.java new file mode 100644 index 000000000..203bab6ff --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyDrawParams.java @@ -0,0 +1,140 @@ +/* + * 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.keyboard.internal; + +import android.graphics.Typeface; + +import com.android.inputmethod.latin.ResourceUtils; + +public class KeyDrawParams { + public Typeface mTypeface; + + public int mLetterSize; + public int mLabelSize; + public int mLargeLetterSize; + public int mLargeLabelSize; + public int mHintLetterSize; + public int mShiftedLetterHintSize; + public int mHintLabelSize; + public int mPreviewTextSize; + + public int mTextColor; + public int mTextInactivatedColor; + public int mTextShadowColor; + public int mHintLetterColor; + public int mHintLabelColor; + public int mShiftedLetterHintInactivatedColor; + public int mShiftedLetterHintActivatedColor; + public int mPreviewTextColor; + + public int mAnimAlpha; + + public KeyDrawParams() {} + + private KeyDrawParams(final KeyDrawParams copyFrom) { + mTypeface = copyFrom.mTypeface; + + mLetterSize = copyFrom.mLetterSize; + mLabelSize = copyFrom.mLabelSize; + mLargeLetterSize = copyFrom.mLargeLetterSize; + mLargeLabelSize = copyFrom.mLargeLabelSize; + mHintLetterSize = copyFrom.mHintLetterSize; + mShiftedLetterHintSize = copyFrom.mShiftedLetterHintSize; + mHintLabelSize = copyFrom.mHintLabelSize; + mPreviewTextSize = copyFrom.mPreviewTextSize; + + mTextColor = copyFrom.mTextColor; + mTextInactivatedColor = copyFrom.mTextInactivatedColor; + mTextShadowColor = copyFrom.mTextShadowColor; + mHintLetterColor = copyFrom.mHintLetterColor; + mHintLabelColor = copyFrom.mHintLabelColor; + mShiftedLetterHintInactivatedColor = copyFrom.mShiftedLetterHintInactivatedColor; + mShiftedLetterHintActivatedColor = copyFrom.mShiftedLetterHintActivatedColor; + mPreviewTextColor = copyFrom.mPreviewTextColor; + + mAnimAlpha = copyFrom.mAnimAlpha; + } + + public void updateParams(final int keyHeight, final KeyVisualAttributes attr) { + if (attr == null) { + return; + } + + if (attr.mTypeface != null) { + mTypeface = attr.mTypeface; + } + + mLetterSize = selectTextSizeFromDimensionOrRatio(keyHeight, + attr.mLetterSize, attr.mLetterRatio, mLetterSize); + mLabelSize = selectTextSizeFromDimensionOrRatio(keyHeight, + attr.mLabelSize, attr.mLabelRatio, mLabelSize); + mLargeLabelSize = selectTextSize(keyHeight, attr.mLargeLabelRatio, mLargeLabelSize); + mLargeLetterSize = selectTextSize(keyHeight, attr.mLargeLetterRatio, mLargeLetterSize); + mHintLetterSize = selectTextSize(keyHeight, attr.mHintLetterRatio, mHintLetterSize); + mShiftedLetterHintSize = selectTextSize(keyHeight, + attr.mShiftedLetterHintRatio, mShiftedLetterHintSize); + mHintLabelSize = selectTextSize(keyHeight, attr.mHintLabelRatio, mHintLabelSize); + mPreviewTextSize = selectTextSize(keyHeight, attr.mPreviewTextRatio, mPreviewTextSize); + + mTextColor = selectColor(attr.mTextColor, mTextColor); + mTextInactivatedColor = selectColor(attr.mTextInactivatedColor, mTextInactivatedColor); + mTextShadowColor = selectColor(attr.mTextShadowColor, mTextShadowColor); + mHintLetterColor = selectColor(attr.mHintLetterColor, mHintLetterColor); + mHintLabelColor = selectColor(attr.mHintLabelColor, mHintLabelColor); + mShiftedLetterHintInactivatedColor = selectColor( + attr.mShiftedLetterHintInactivatedColor, mShiftedLetterHintInactivatedColor); + mShiftedLetterHintActivatedColor = selectColor( + attr.mShiftedLetterHintActivatedColor, mShiftedLetterHintActivatedColor); + mPreviewTextColor = selectColor(attr.mPreviewTextColor, mPreviewTextColor); + } + + public KeyDrawParams mayCloneAndUpdateParams(final int keyHeight, + final KeyVisualAttributes attr) { + if (attr == null) { + return this; + } + final KeyDrawParams newParams = new KeyDrawParams(this); + newParams.updateParams(keyHeight, attr); + return newParams; + } + + private static final int selectTextSizeFromDimensionOrRatio(final int keyHeight, + final int dimens, final float ratio, final int defaultDimens) { + if (ResourceUtils.isValidDimensionPixelSize(dimens)) { + return dimens; + } + if (ResourceUtils.isValidFraction(ratio)) { + return (int)(keyHeight * ratio); + } + return defaultDimens; + } + + private static final int selectTextSize(final int keyHeight, final float ratio, + final int defaultSize) { + if (ResourceUtils.isValidFraction(ratio)) { + return (int)(keyHeight * ratio); + } + return defaultSize; + } + + private static final int selectColor(final int attrColor, final int defaultColor) { + if (attrColor != 0) { + return attrColor; + } + return defaultColor; + } +} diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewDrawParams.java b/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewDrawParams.java new file mode 100644 index 000000000..996a722c0 --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewDrawParams.java @@ -0,0 +1,46 @@ +/* + * 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.keyboard.internal; + +public class KeyPreviewDrawParams { + // The graphical geometry of the key preview. + // <-width-> + // +-------+ ^ + // | | | + // |preview| height (visible) + // | | | + // + + ^ v + // \ / |offset + // +-\ /-+ v + // | +-+ | + // |parent | + // | key| + // +-------+ + // The background of a {@link TextView} being used for a key preview may have invisible + // paddings. To align the more keys keyboard panel's visible part with the visible part of + // the background, we need to record the width and height of key preview that don't include + // invisible paddings. + public int mPreviewVisibleWidth; + public int mPreviewVisibleHeight; + // The key preview may have an arbitrary offset and its background that may have a bottom + // padding. To align the more keys keyboard and the key preview we also need to record the + // offset between the top edge of parent key and the bottom of the visible part of key + // preview background. + public int mPreviewVisibleOffset; + + public final int[] mCoordinates = new int[2]; +} diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java b/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java index 94a7b826f..2a57caa5f 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java @@ -21,6 +21,7 @@ import static com.android.inputmethod.keyboard.Keyboard.CODE_UNSPECIFIED; import android.text.TextUtils; import com.android.inputmethod.keyboard.Keyboard; +import com.android.inputmethod.latin.CollectionUtils; import com.android.inputmethod.latin.LatinImeLogger; import com.android.inputmethod.latin.StringUtils; @@ -55,59 +56,20 @@ public class KeySpecParser { private static final char ESCAPE_CHAR = '\\'; private static final char LABEL_END = '|'; private static final String PREFIX_TEXT = "!text/"; - private static final String PREFIX_ICON = "!icon/"; + static final String PREFIX_ICON = "!icon/"; private static final String PREFIX_CODE = "!code/"; private static final String PREFIX_HEX = "0x"; private static final String ADDITIONAL_MORE_KEY_MARKER = "%"; - public static class MoreKeySpec { - public final int mCode; - public final String mLabel; - public final String mOutputText; - public final int mIconId; - - public MoreKeySpec(final String moreKeySpec, boolean needsToUpperCase, Locale locale, - final KeyboardCodesSet codesSet) { - mLabel = toUpperCaseOfStringForLocale(getLabel(moreKeySpec), - needsToUpperCase, locale); - final int code = toUpperCaseOfCodeForLocale(getCode(moreKeySpec, codesSet), - needsToUpperCase, locale); - if (code == Keyboard.CODE_UNSPECIFIED) { - // Some letter, for example German Eszett (U+00DF: "ß"), has multiple characters - // upper case representation ("SS"). - mCode = Keyboard.CODE_OUTPUT_TEXT; - mOutputText = mLabel; - } else { - mCode = code; - mOutputText = toUpperCaseOfStringForLocale(getOutputText(moreKeySpec), - needsToUpperCase, locale); - } - mIconId = getIconId(moreKeySpec); - } - - @Override - public String toString() { - final String label = (mIconId == KeyboardIconsSet.ICON_UNDEFINED ? mLabel - : PREFIX_ICON + KeyboardIconsSet.getIconName(mIconId)); - final String output = (mCode == Keyboard.CODE_OUTPUT_TEXT ? mOutputText - : Keyboard.printableCode(mCode)); - if (StringUtils.codePointCount(label) == 1 && label.codePointAt(0) == mCode) { - return output; - } else { - return label + "|" + output; - } - } - } - private KeySpecParser() { // Intentional empty constructor for utility class. } - private static boolean hasIcon(String moreKeySpec) { + private static boolean hasIcon(final String moreKeySpec) { return moreKeySpec.startsWith(PREFIX_ICON); } - private static boolean hasCode(String moreKeySpec) { + private static boolean hasCode(final String moreKeySpec) { final int end = indexOfLabelEnd(moreKeySpec, 0); if (end > 0 && end + 1 < moreKeySpec.length() && moreKeySpec.startsWith( PREFIX_CODE, end + 1)) { @@ -116,7 +78,7 @@ public class KeySpecParser { return false; } - private static String parseEscape(String text) { + private static String parseEscape(final String text) { if (text.indexOf(ESCAPE_CHAR) < 0) { return text; } @@ -135,7 +97,7 @@ public class KeySpecParser { return sb.toString(); } - private static int indexOfLabelEnd(String moreKeySpec, int start) { + private static int indexOfLabelEnd(final String moreKeySpec, final int start) { if (moreKeySpec.indexOf(ESCAPE_CHAR, start) < 0) { final int end = moreKeySpec.indexOf(LABEL_END, start); if (end == 0) { @@ -156,7 +118,7 @@ public class KeySpecParser { return -1; } - public static String getLabel(String moreKeySpec) { + public static String getLabel(final String moreKeySpec) { if (hasIcon(moreKeySpec)) { return null; } @@ -169,7 +131,7 @@ public class KeySpecParser { return label; } - private static String getOutputTextInternal(String moreKeySpec) { + private static String getOutputTextInternal(final String moreKeySpec) { final int end = indexOfLabelEnd(moreKeySpec, 0); if (end <= 0) { return null; @@ -180,7 +142,7 @@ public class KeySpecParser { return parseEscape(moreKeySpec.substring(end + /* LABEL_END */1)); } - static String getOutputText(String moreKeySpec) { + static String getOutputText(final String moreKeySpec) { if (hasCode(moreKeySpec)) { return null; } @@ -204,7 +166,7 @@ public class KeySpecParser { return (StringUtils.codePointCount(label) == 1) ? null : label; } - static int getCode(String moreKeySpec, KeyboardCodesSet codesSet) { + static int getCode(final String moreKeySpec, final KeyboardCodesSet codesSet) { if (hasCode(moreKeySpec)) { final int end = indexOfLabelEnd(moreKeySpec, 0); if (indexOfLabelEnd(moreKeySpec, end + 1) >= 0) { @@ -229,7 +191,8 @@ public class KeySpecParser { return Keyboard.CODE_OUTPUT_TEXT; } - public static int parseCode(String text, KeyboardCodesSet codesSet, int defCode) { + public static int parseCode(final String text, final KeyboardCodesSet codesSet, + final int defCode) { if (text == null) return defCode; if (text.startsWith(PREFIX_CODE)) { return codesSet.getCode(text.substring(PREFIX_CODE.length())); @@ -240,7 +203,7 @@ public class KeySpecParser { } } - public static int getIconId(String moreKeySpec) { + public static int getIconId(final String moreKeySpec) { if (moreKeySpec != null && hasIcon(moreKeySpec)) { final int end = moreKeySpec.indexOf(LABEL_END, PREFIX_ICON.length()); final String name = (end < 0) ? moreKeySpec.substring(PREFIX_ICON.length()) @@ -250,7 +213,7 @@ public class KeySpecParser { return KeyboardIconsSet.ICON_UNDEFINED; } - private static <T> ArrayList<T> arrayAsList(T[] array, int start, int end) { + private static <T> ArrayList<T> arrayAsList(final T[] array, final int start, final int end) { if (array == null) { throw new NullPointerException(); } @@ -258,7 +221,7 @@ public class KeySpecParser { throw new IllegalArgumentException(); } - final ArrayList<T> list = new ArrayList<T>(end - start); + final ArrayList<T> list = CollectionUtils.newArrayList(end - start); for (int i = start; i < end; i++) { list.add(array[i]); } @@ -267,7 +230,7 @@ public class KeySpecParser { private static final String[] EMPTY_STRING_ARRAY = new String[0]; - private static String[] filterOutEmptyString(String[] array) { + private static String[] filterOutEmptyString(final String[] array) { if (array == null) { return EMPTY_STRING_ARRAY; } @@ -288,8 +251,8 @@ public class KeySpecParser { return out.toArray(new String[out.size()]); } - public static String[] insertAdditionalMoreKeys(String[] moreKeySpecs, - String[] additionalMoreKeySpecs) { + public static String[] insertAdditionalMoreKeys(final String[] moreKeySpecs, + final String[] additionalMoreKeySpecs) { final String[] moreKeys = filterOutEmptyString(moreKeySpecs); final String[] additionalMoreKeys = filterOutEmptyString(additionalMoreKeySpecs); final int moreKeysCount = moreKeys.length; @@ -356,12 +319,13 @@ public class KeySpecParser { @SuppressWarnings("serial") public static class KeySpecParserError extends RuntimeException { - public KeySpecParserError(String message) { + public KeySpecParserError(final String message) { super(message); } } - public static String resolveTextReference(String rawText, KeyboardTextsSet textsSet) { + public static String resolveTextReference(final String rawText, + final KeyboardTextsSet textsSet) { int level = 0; String text = rawText; StringBuilder sb; @@ -407,7 +371,7 @@ public class KeySpecParser { return text; } - private static int searchTextNameEnd(String text, int start) { + private static int searchTextNameEnd(final String text, final int start) { final int size = text.length(); for (int pos = start; pos < size; pos++) { final char c = text.charAt(pos); @@ -420,7 +384,7 @@ public class KeySpecParser { return size; } - public static String[] parseCsvString(String rawText, KeyboardTextsSet textsSet) { + public static String[] parseCsvString(final String rawText, final KeyboardTextsSet textsSet) { final String text = resolveTextReference(rawText, textsSet); final int size = text.length(); if (size == 0) { @@ -438,7 +402,7 @@ public class KeySpecParser { // Skip empty entry. if (pos - start > 0) { if (list == null) { - list = new ArrayList<String>(); + list = CollectionUtils.newArrayList(); } list.add(text.substring(start, pos)); } @@ -459,7 +423,8 @@ public class KeySpecParser { return list.toArray(new String[list.size()]); } - public static int getIntValue(String[] moreKeys, String key, int defaultValue) { + public static int getIntValue(final String[] moreKeys, final String key, + final int defaultValue) { if (moreKeys == null) { return defaultValue; } @@ -485,7 +450,7 @@ public class KeySpecParser { return value; } - public static boolean getBooleanValue(String[] moreKeys, String key) { + public static boolean getBooleanValue(final String[] moreKeys, final String key) { if (moreKeys == null) { return false; } @@ -501,8 +466,8 @@ public class KeySpecParser { return value; } - public static int toUpperCaseOfCodeForLocale(int code, boolean needsToUpperCase, - Locale locale) { + public static int toUpperCaseOfCodeForLocale(final int code, final boolean needsToUpperCase, + final Locale locale) { if (!Keyboard.isLetterCode(code) || !needsToUpperCase) return code; final String text = new String(new int[] { code } , 0, 1); final String casedText = KeySpecParser.toUpperCaseOfStringForLocale( @@ -511,8 +476,8 @@ public class KeySpecParser { ? casedText.codePointAt(0) : CODE_UNSPECIFIED; } - public static String toUpperCaseOfStringForLocale(String text, boolean needsToUpperCase, - Locale locale) { + public static String toUpperCaseOfStringForLocale(final String text, + final boolean needsToUpperCase, final Locale locale) { if (text == null || !needsToUpperCase) return text; return text.toUpperCase(locale); } diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java b/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java new file mode 100644 index 000000000..e8cacf9e7 --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java @@ -0,0 +1,46 @@ +/* + * 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.keyboard.internal; + +import android.content.res.TypedArray; + +public abstract class KeyStyle { + private final KeyboardTextsSet mTextsSet; + + public abstract String[] getStringArray(TypedArray a, int index); + public abstract String getString(TypedArray a, int index); + public abstract int getInt(TypedArray a, int index, int defaultValue); + public abstract int getFlag(TypedArray a, int index); + + protected KeyStyle(final KeyboardTextsSet textsSet) { + mTextsSet = textsSet; + } + + protected String parseString(final TypedArray a, final int index) { + if (a.hasValue(index)) { + return KeySpecParser.resolveTextReference(a.getString(index), mTextsSet); + } + return null; + } + + protected String[] parseStringArray(final TypedArray a, final int index) { + if (a.hasValue(index)) { + return KeySpecParser.parseCsvString(a.getString(index), mTextsSet); + } + return null; + } +} diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyStyles.java b/java/src/com/android/inputmethod/keyboard/internal/KeyStylesSet.java index 291b3b943..71fd30563 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyStyles.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyStylesSet.java @@ -20,7 +20,7 @@ import android.content.res.TypedArray; import android.util.Log; import android.util.SparseArray; -import com.android.inputmethod.keyboard.Keyboard; +import com.android.inputmethod.latin.CollectionUtils; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.XmlParseUtils; @@ -29,75 +29,62 @@ import org.xmlpull.v1.XmlPullParserException; import java.util.HashMap; -public class KeyStyles { - private static final String TAG = KeyStyles.class.getSimpleName(); +public class KeyStylesSet { + private static final String TAG = KeyStylesSet.class.getSimpleName(); private static final boolean DEBUG = false; - final HashMap<String, KeyStyle> mStyles = new HashMap<String, KeyStyle>(); + private final HashMap<String, KeyStyle> mStyles = CollectionUtils.newHashMap(); - final KeyboardTextsSet mTextsSet; + private final KeyboardTextsSet mTextsSet; private final KeyStyle mEmptyKeyStyle; private static final String EMPTY_STYLE_NAME = "<empty>"; - public KeyStyles(KeyboardTextsSet textsSet) { + public KeyStylesSet(final KeyboardTextsSet textsSet) { mTextsSet = textsSet; - mEmptyKeyStyle = new EmptyKeyStyle(); + mEmptyKeyStyle = new EmptyKeyStyle(textsSet); mStyles.put(EMPTY_STYLE_NAME, mEmptyKeyStyle); } - public abstract class KeyStyle { - public abstract String[] getStringArray(TypedArray a, int index); - public abstract String getString(TypedArray a, int index); - public abstract int getInt(TypedArray a, int index, int defaultValue); - public abstract int getFlag(TypedArray a, int index); - - protected String parseString(TypedArray a, int index) { - if (a.hasValue(index)) { - return KeySpecParser.resolveTextReference(a.getString(index), mTextsSet); - } - return null; - } - - protected String[] parseStringArray(TypedArray a, int index) { - if (a.hasValue(index)) { - return KeySpecParser.parseCsvString(a.getString(index), mTextsSet); - } - return null; + private static class EmptyKeyStyle extends KeyStyle { + EmptyKeyStyle(final KeyboardTextsSet textsSet) { + super(textsSet); } - } - class EmptyKeyStyle extends KeyStyle { @Override - public String[] getStringArray(TypedArray a, int index) { + public String[] getStringArray(final TypedArray a, final int index) { return parseStringArray(a, index); } @Override - public String getString(TypedArray a, int index) { + public String getString(final TypedArray a, final int index) { return parseString(a, index); } @Override - public int getInt(TypedArray a, int index, int defaultValue) { + public int getInt(final TypedArray a, final int index, final int defaultValue) { return a.getInt(index, defaultValue); } @Override - public int getFlag(TypedArray a, int index) { + public int getFlag(final TypedArray a, final int index) { return a.getInt(index, 0); } } - private class DeclaredKeyStyle extends KeyStyle { + private static class DeclaredKeyStyle extends KeyStyle { + private final HashMap<String, KeyStyle> mStyles; private final String mParentStyleName; - private final SparseArray<Object> mStyleAttributes = new SparseArray<Object>(); + private final SparseArray<Object> mStyleAttributes = CollectionUtils.newSparseArray(); - public DeclaredKeyStyle(String parentStyleName) { + public DeclaredKeyStyle(final String parentStyleName, final KeyboardTextsSet textsSet, + final HashMap<String, KeyStyle> styles) { + super(textsSet); mParentStyleName = parentStyleName; + mStyles = styles; } @Override - public String[] getStringArray(TypedArray a, int index) { + public String[] getStringArray(final TypedArray a, final int index) { if (a.hasValue(index)) { return parseStringArray(a, index); } @@ -110,7 +97,7 @@ public class KeyStyles { } @Override - public String getString(TypedArray a, int index) { + public String getString(final TypedArray a, final int index) { if (a.hasValue(index)) { return parseString(a, index); } @@ -123,7 +110,7 @@ public class KeyStyles { } @Override - public int getInt(TypedArray a, int index, int defaultValue) { + public int getInt(final TypedArray a, final int index, final int defaultValue) { if (a.hasValue(index)) { return a.getInt(index, defaultValue); } @@ -136,7 +123,7 @@ public class KeyStyles { } @Override - public int getFlag(TypedArray a, int index) { + public int getFlag(final TypedArray a, final int index) { int flags = a.getInt(index, 0); final Object value = mStyleAttributes.get(index); if (value != null) { @@ -146,7 +133,7 @@ public class KeyStyles { return flags | parentStyle.getFlag(a, index); } - void readKeyAttributes(TypedArray keyAttr) { + public void readKeyAttributes(final TypedArray keyAttr) { // TODO: Currently not all Key attributes can be declared as style. readString(keyAttr, R.styleable.Keyboard_Key_code); readString(keyAttr, R.styleable.Keyboard_Key_altCode); @@ -164,38 +151,38 @@ public class KeyStyles { readFlag(keyAttr, R.styleable.Keyboard_Key_keyActionFlags); } - private void readString(TypedArray a, int index) { + private void readString(final TypedArray a, final int index) { if (a.hasValue(index)) { mStyleAttributes.put(index, parseString(a, index)); } } - private void readInt(TypedArray a, int index) { + private void readInt(final TypedArray a, final int index) { if (a.hasValue(index)) { mStyleAttributes.put(index, a.getInt(index, 0)); } } - private void readFlag(TypedArray a, int index) { + private void readFlag(final TypedArray a, final int index) { if (a.hasValue(index)) { final Integer value = (Integer)mStyleAttributes.get(index); mStyleAttributes.put(index, a.getInt(index, 0) | (value != null ? value : 0)); } } - private void readStringArray(TypedArray a, int index) { + private void readStringArray(final TypedArray a, final int index) { if (a.hasValue(index)) { mStyleAttributes.put(index, parseStringArray(a, index)); } } } - public void parseKeyStyleAttributes(TypedArray keyStyleAttr, TypedArray keyAttrs, - XmlPullParser parser) throws XmlPullParserException { + public void parseKeyStyleAttributes(final TypedArray keyStyleAttr, final TypedArray keyAttrs, + final XmlPullParser parser) throws XmlPullParserException { final String styleName = keyStyleAttr.getString(R.styleable.Keyboard_KeyStyle_styleName); if (DEBUG) { Log.d(TAG, String.format("<%s styleName=%s />", - Keyboard.Builder.TAG_KEY_STYLE, styleName)); + KeyboardBuilder.TAG_KEY_STYLE, styleName)); if (mStyles.containsKey(styleName)) { Log.d(TAG, "key-style " + styleName + " is overridden at " + parser.getPositionDescription()); @@ -210,12 +197,12 @@ public class KeyStyles { "Unknown parentStyle " + parentStyleName, parser); } } - final DeclaredKeyStyle style = new DeclaredKeyStyle(parentStyleName); + final DeclaredKeyStyle style = new DeclaredKeyStyle(parentStyleName, mTextsSet, mStyles); style.readKeyAttributes(keyAttrs); mStyles.put(styleName, style); } - public KeyStyle getKeyStyle(TypedArray keyAttr, XmlPullParser parser) + public KeyStyle getKeyStyle(final TypedArray keyAttr, final XmlPullParser parser) throws XmlParseUtils.ParseException { if (!keyAttr.hasValue(R.styleable.Keyboard_Key_keyStyle)) { return mEmptyKeyStyle; diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyVisualAttributes.java b/java/src/com/android/inputmethod/keyboard/internal/KeyVisualAttributes.java new file mode 100644 index 000000000..04cc152fe --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyVisualAttributes.java @@ -0,0 +1,130 @@ +/* + * 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.keyboard.internal; + +import android.content.res.TypedArray; +import android.graphics.Typeface; +import android.util.SparseIntArray; + +import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.ResourceUtils; + +public class KeyVisualAttributes { + public final Typeface mTypeface; + + public final float mLetterRatio; + public final int mLetterSize; + public final float mLabelRatio; + public final int mLabelSize; + public final float mLargeLetterRatio; + public final float mLargeLabelRatio; + public final float mHintLetterRatio; + public final float mShiftedLetterHintRatio; + public final float mHintLabelRatio; + public final float mPreviewTextRatio; + + public final int mTextColor; + public final int mTextInactivatedColor; + public final int mTextShadowColor; + public final int mHintLetterColor; + public final int mHintLabelColor; + public final int mShiftedLetterHintInactivatedColor; + public final int mShiftedLetterHintActivatedColor; + public final int mPreviewTextColor; + + private static final int[] VISUAL_ATTRIBUTE_IDS = { + R.styleable.Keyboard_Key_keyTypeface, + R.styleable.Keyboard_Key_keyLetterSize, + R.styleable.Keyboard_Key_keyLabelSize, + R.styleable.Keyboard_Key_keyLargeLetterRatio, + R.styleable.Keyboard_Key_keyLargeLabelRatio, + R.styleable.Keyboard_Key_keyHintLetterRatio, + R.styleable.Keyboard_Key_keyShiftedLetterHintRatio, + R.styleable.Keyboard_Key_keyHintLabelRatio, + R.styleable.Keyboard_Key_keyPreviewTextRatio, + R.styleable.Keyboard_Key_keyTextColor, + R.styleable.Keyboard_Key_keyTextInactivatedColor, + R.styleable.Keyboard_Key_keyTextShadowColor, + R.styleable.Keyboard_Key_keyHintLetterColor, + R.styleable.Keyboard_Key_keyHintLabelColor, + R.styleable.Keyboard_Key_keyShiftedLetterHintInactivatedColor, + R.styleable.Keyboard_Key_keyShiftedLetterHintActivatedColor, + R.styleable.Keyboard_Key_keyPreviewTextColor, + }; + private static final SparseIntArray sVisualAttributeIds = new SparseIntArray(); + private static final int ATTR_DEFINED = 1; + private static final int ATTR_NOT_FOUND = 0; + static { + for (final int attrId : VISUAL_ATTRIBUTE_IDS) { + sVisualAttributeIds.put(attrId, ATTR_DEFINED); + } + } + + public static KeyVisualAttributes newInstance(final TypedArray keyAttr) { + final int indexCount = keyAttr.getIndexCount(); + for (int i = 0; i < indexCount; i++) { + final int attrId = keyAttr.getIndex(i); + if (sVisualAttributeIds.get(attrId, ATTR_NOT_FOUND) == ATTR_NOT_FOUND) { + continue; + } + return new KeyVisualAttributes(keyAttr); + } + return null; + } + + private KeyVisualAttributes(final TypedArray keyAttr) { + if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyTypeface)) { + mTypeface = Typeface.defaultFromStyle( + keyAttr.getInt(R.styleable.Keyboard_Key_keyTypeface, Typeface.NORMAL)); + } else { + mTypeface = null; + } + + mLetterRatio = ResourceUtils.getFraction(keyAttr, + R.styleable.Keyboard_Key_keyLetterSize); + mLetterSize = ResourceUtils.getDimensionPixelSize(keyAttr, + R.styleable.Keyboard_Key_keyLetterSize); + mLabelRatio = ResourceUtils.getFraction(keyAttr, + R.styleable.Keyboard_Key_keyLabelSize); + mLabelSize = ResourceUtils.getDimensionPixelSize(keyAttr, + R.styleable.Keyboard_Key_keyLabelSize); + mLargeLetterRatio = ResourceUtils.getFraction(keyAttr, + R.styleable.Keyboard_Key_keyLargeLetterRatio); + mLargeLabelRatio = ResourceUtils.getFraction(keyAttr, + R.styleable.Keyboard_Key_keyLargeLabelRatio); + mHintLetterRatio = ResourceUtils.getFraction(keyAttr, + R.styleable.Keyboard_Key_keyHintLetterRatio); + mShiftedLetterHintRatio = ResourceUtils.getFraction(keyAttr, + R.styleable.Keyboard_Key_keyShiftedLetterHintRatio); + mHintLabelRatio = ResourceUtils.getFraction(keyAttr, + R.styleable.Keyboard_Key_keyHintLabelRatio); + mPreviewTextRatio = ResourceUtils.getFraction(keyAttr, + R.styleable.Keyboard_Key_keyPreviewTextRatio); + + mTextColor = keyAttr.getColor(R.styleable.Keyboard_Key_keyTextColor, 0); + mTextInactivatedColor = keyAttr.getColor( + R.styleable.Keyboard_Key_keyTextInactivatedColor, 0); + mTextShadowColor = keyAttr.getColor(R.styleable.Keyboard_Key_keyTextShadowColor, 0); + mHintLetterColor = keyAttr.getColor(R.styleable.Keyboard_Key_keyHintLetterColor, 0); + mHintLabelColor = keyAttr.getColor(R.styleable.Keyboard_Key_keyHintLabelColor, 0); + mShiftedLetterHintInactivatedColor = keyAttr.getColor( + R.styleable.Keyboard_Key_keyShiftedLetterHintInactivatedColor, 0); + mShiftedLetterHintActivatedColor = keyAttr.getColor( + R.styleable.Keyboard_Key_keyShiftedLetterHintActivatedColor, 0); + mPreviewTextColor = keyAttr.getColor(R.styleable.Keyboard_Key_keyPreviewTextColor, 0); + } +} diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java new file mode 100644 index 000000000..31c7cb565 --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java @@ -0,0 +1,814 @@ +/* + * 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.keyboard.internal; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.TypedValue; +import android.util.Xml; +import android.view.InflateException; + +import com.android.inputmethod.keyboard.Key; +import com.android.inputmethod.keyboard.Keyboard; +import com.android.inputmethod.keyboard.KeyboardId; +import com.android.inputmethod.latin.LocaleUtils.RunInLocale; +import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.ResourceUtils; +import com.android.inputmethod.latin.StringUtils; +import com.android.inputmethod.latin.SubtypeLocale; +import com.android.inputmethod.latin.XmlParseUtils; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Locale; + +/** + * Keyboard Building helper. + * + * This class parses Keyboard XML file and eventually build a Keyboard. + * The Keyboard XML file looks like: + * <pre> + * <!-- xml/keyboard.xml --> + * <Keyboard keyboard_attributes*> + * <!-- Keyboard Content --> + * <Row row_attributes*> + * <!-- Row Content --> + * <Key key_attributes* /> + * <Spacer horizontalGap="32.0dp" /> + * <include keyboardLayout="@xml/other_keys"> + * ... + * </Row> + * <include keyboardLayout="@xml/other_rows"> + * ... + * </Keyboard> + * </pre> + * The XML file which is included in other file must have <merge> as root element, + * such as: + * <pre> + * <!-- xml/other_keys.xml --> + * <merge> + * <Key key_attributes* /> + * ... + * </merge> + * </pre> + * and + * <pre> + * <!-- xml/other_rows.xml --> + * <merge> + * <Row row_attributes*> + * <Key key_attributes* /> + * </Row> + * ... + * </merge> + * </pre> + * You can also use switch-case-default tags to select Rows and Keys. + * <pre> + * <switch> + * <case case_attribute*> + * <!-- Any valid tags at switch position --> + * </case> + * ... + * <default> + * <!-- Any valid tags at switch position --> + * </default> + * </switch> + * </pre> + * You can declare Key style and specify styles within Key tags. + * <pre> + * <switch> + * <case mode="email"> + * <key-style styleName="f1-key" parentStyle="modifier-key" + * keyLabel=".com" + * /> + * </case> + * <case mode="url"> + * <key-style styleName="f1-key" parentStyle="modifier-key" + * keyLabel="http://" + * /> + * </case> + * </switch> + * ... + * <Key keyStyle="shift-key" ... /> + * </pre> + */ + +public class KeyboardBuilder<KP extends KeyboardParams> { + private static final String BUILDER_TAG = "Keyboard.Builder"; + private static final boolean DEBUG = false; + + // Keyboard XML Tags + private static final String TAG_KEYBOARD = "Keyboard"; + private static final String TAG_ROW = "Row"; + private static final String TAG_KEY = "Key"; + private static final String TAG_SPACER = "Spacer"; + private static final String TAG_INCLUDE = "include"; + private static final String TAG_MERGE = "merge"; + private static final String TAG_SWITCH = "switch"; + private static final String TAG_CASE = "case"; + private static final String TAG_DEFAULT = "default"; + public static final String TAG_KEY_STYLE = "key-style"; + + private static final int DEFAULT_KEYBOARD_COLUMNS = 10; + private static final int DEFAULT_KEYBOARD_ROWS = 4; + + protected final KP mParams; + protected final Context mContext; + protected final Resources mResources; + private final DisplayMetrics mDisplayMetrics; + + private int mCurrentY = 0; + private KeyboardRow mCurrentRow = null; + private boolean mLeftEdge; + private boolean mTopEdge; + private Key mRightEdgeKey = null; + + public KeyboardBuilder(final Context context, final KP params) { + mContext = context; + final Resources res = context.getResources(); + mResources = res; + mDisplayMetrics = res.getDisplayMetrics(); + + mParams = params; + + params.GRID_WIDTH = res.getInteger(R.integer.config_keyboard_grid_width); + params.GRID_HEIGHT = res.getInteger(R.integer.config_keyboard_grid_height); + } + + public void setAutoGenerate(final KeysCache keysCache) { + mParams.mKeysCache = keysCache; + } + + public KeyboardBuilder<KP> load(final int xmlId, final KeyboardId id) { + mParams.mId = id; + final XmlResourceParser parser = mResources.getXml(xmlId); + try { + parseKeyboard(parser); + } catch (XmlPullParserException e) { + Log.w(BUILDER_TAG, "keyboard XML parse error: " + e); + throw new IllegalArgumentException(e); + } catch (IOException e) { + Log.w(BUILDER_TAG, "keyboard XML parse error: " + e); + throw new RuntimeException(e); + } finally { + parser.close(); + } + return this; + } + + // TODO: Remove this method. + public void setTouchPositionCorrectionEnabled(final boolean enabled) { + mParams.mTouchPositionCorrection.setEnabled(enabled); + } + + public void setProximityCharsCorrectionEnabled(final boolean enabled) { + mParams.mProximityCharsCorrectionEnabled = enabled; + } + + public Keyboard build() { + return new Keyboard(mParams); + } + + private int mIndent; + private static final String SPACES = " "; + + private static String spaces(final int count) { + return (count < SPACES.length()) ? SPACES.substring(0, count) : SPACES; + } + + private void startTag(final String format, final Object ... args) { + Log.d(BUILDER_TAG, String.format(spaces(++mIndent * 2) + format, args)); + } + + private void endTag(final String format, final Object ... args) { + Log.d(BUILDER_TAG, String.format(spaces(mIndent-- * 2) + format, args)); + } + + private void startEndTag(final String format, final Object ... args) { + Log.d(BUILDER_TAG, String.format(spaces(++mIndent * 2) + format, args)); + mIndent--; + } + + private void parseKeyboard(final XmlPullParser parser) + throws XmlPullParserException, IOException { + if (DEBUG) startTag("<%s> %s", TAG_KEYBOARD, mParams.mId); + int event; + while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) { + if (event == XmlPullParser.START_TAG) { + final String tag = parser.getName(); + if (TAG_KEYBOARD.equals(tag)) { + parseKeyboardAttributes(parser); + startKeyboard(); + parseKeyboardContent(parser, false); + break; + } else { + throw new XmlParseUtils.IllegalStartTag(parser, TAG_KEYBOARD); + } + } + } + } + + 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, null); + final float keyboardHeight; + if (keyboardHeightString != null) { + keyboardHeight = Float.parseFloat(keyboardHeightString) + * mDisplayMetrics.density; + } else { + keyboardHeight = keyboardAttr.getDimension( + R.styleable.Keyboard_keyboardHeight, displayHeight / 2); + } + 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 + - params.mBottomPadding + params.mVerticalGap; + params.mDefaultRowHeight = (int)ResourceUtils.getDimensionOrFraction(keyboardAttr, + R.styleable.Keyboard_rowHeight, params.mBaseHeight, + params.mBaseHeight / DEFAULT_KEYBOARD_ROWS); + + params.mKeyVisualAttributes = KeyVisualAttributes.newInstance(keyAttr); + + params.mMoreKeysTemplate = keyboardAttr.getResourceId( + R.styleable.Keyboard_moreKeysTemplate, 0); + params.mMaxMoreKeysKeyboardColumn = keyAttr.getInt( + R.styleable.Keyboard_Key_maxMoreKeysColumn, 5); + + params.mThemeId = keyboardAttr.getInt(R.styleable.Keyboard_themeId, 0); + params.mIconsSet.loadIcons(keyboardAttr); + final String language = params.mId.mLocale.getLanguage(); + params.mCodesSet.setLanguage(language); + params.mTextsSet.setLanguage(language); + final RunInLocale<Void> job = new RunInLocale<Void>() { + @Override + protected Void job(Resources res) { + params.mTextsSet.loadStringResources(mContext); + return null; + } + }; + // Null means the current system locale. + final Locale locale = SubtypeLocale.isNoLanguage(params.mId.mSubtype) + ? null : params.mId.mLocale; + job.runInLocale(mResources, locale); + + final int resourceId = keyboardAttr.getResourceId( + R.styleable.Keyboard_touchPositionCorrectionData, 0); + params.mTouchPositionCorrection.setEnabled(resourceId != 0); + if (resourceId != 0) { + final String[] data = mResources.getStringArray(resourceId); + params.mTouchPositionCorrection.load(data); + } + } finally { + keyAttr.recycle(); + keyboardAttr.recycle(); + } + } + + private void parseKeyboardContent(final XmlPullParser parser, final boolean skip) + throws XmlPullParserException, IOException { + int event; + while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) { + if (event == XmlPullParser.START_TAG) { + final String tag = parser.getName(); + if (TAG_ROW.equals(tag)) { + final KeyboardRow row = parseRowAttributes(parser); + if (DEBUG) startTag("<%s>%s", TAG_ROW, skip ? " skipped" : ""); + if (!skip) { + startRow(row); + } + parseRowContent(parser, row, skip); + } else if (TAG_INCLUDE.equals(tag)) { + parseIncludeKeyboardContent(parser, skip); + } else if (TAG_SWITCH.equals(tag)) { + parseSwitchKeyboardContent(parser, skip); + } else if (TAG_KEY_STYLE.equals(tag)) { + parseKeyStyle(parser, skip); + } else { + throw new XmlParseUtils.IllegalStartTag(parser, TAG_ROW); + } + } else if (event == XmlPullParser.END_TAG) { + final String tag = parser.getName(); + if (DEBUG) endTag("</%s>", tag); + if (TAG_KEYBOARD.equals(tag)) { + endKeyboard(); + break; + } else if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag) + || TAG_MERGE.equals(tag)) { + break; + } else { + throw new XmlParseUtils.IllegalEndTag(parser, TAG_ROW); + } + } + } + } + + private KeyboardRow parseRowAttributes(final XmlPullParser parser) + throws XmlPullParserException { + final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser), + R.styleable.Keyboard); + try { + if (a.hasValue(R.styleable.Keyboard_horizontalGap)) { + throw new XmlParseUtils.IllegalAttribute(parser, "horizontalGap"); + } + if (a.hasValue(R.styleable.Keyboard_verticalGap)) { + throw new XmlParseUtils.IllegalAttribute(parser, "verticalGap"); + } + return new KeyboardRow(mResources, mParams, parser, mCurrentY); + } finally { + a.recycle(); + } + } + + private void parseRowContent(final XmlPullParser parser, final KeyboardRow row, + final boolean skip) throws XmlPullParserException, IOException { + int event; + while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) { + if (event == XmlPullParser.START_TAG) { + final String tag = parser.getName(); + if (TAG_KEY.equals(tag)) { + parseKey(parser, row, skip); + } else if (TAG_SPACER.equals(tag)) { + parseSpacer(parser, row, skip); + } else if (TAG_INCLUDE.equals(tag)) { + parseIncludeRowContent(parser, row, skip); + } else if (TAG_SWITCH.equals(tag)) { + parseSwitchRowContent(parser, row, skip); + } else if (TAG_KEY_STYLE.equals(tag)) { + parseKeyStyle(parser, skip); + } else { + throw new XmlParseUtils.IllegalStartTag(parser, TAG_KEY); + } + } else if (event == XmlPullParser.END_TAG) { + final String tag = parser.getName(); + if (DEBUG) endTag("</%s>", tag); + if (TAG_ROW.equals(tag)) { + if (!skip) { + endRow(row); + } + break; + } else if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag) + || TAG_MERGE.equals(tag)) { + break; + } else { + throw new XmlParseUtils.IllegalEndTag(parser, TAG_KEY); + } + } + } + } + + private void parseKey(final XmlPullParser parser, final KeyboardRow row, final boolean skip) + throws XmlPullParserException, IOException { + if (skip) { + XmlParseUtils.checkEndTag(TAG_KEY, parser); + if (DEBUG) { + startEndTag("<%s /> skipped", TAG_KEY); + } + } else { + final Key key = new Key(mResources, mParams, row, parser); + if (DEBUG) { + startEndTag("<%s%s %s moreKeys=%s />", TAG_KEY, + (key.isEnabled() ? "" : " disabled"), key, + Arrays.toString(key.mMoreKeys)); + } + XmlParseUtils.checkEndTag(TAG_KEY, parser); + endKey(key); + } + } + + private void parseSpacer(final XmlPullParser parser, final KeyboardRow row, final boolean skip) + throws XmlPullParserException, IOException { + if (skip) { + XmlParseUtils.checkEndTag(TAG_SPACER, parser); + if (DEBUG) startEndTag("<%s /> skipped", TAG_SPACER); + } else { + final Key.Spacer spacer = new Key.Spacer(mResources, mParams, row, parser); + if (DEBUG) startEndTag("<%s />", TAG_SPACER); + XmlParseUtils.checkEndTag(TAG_SPACER, parser); + endKey(spacer); + } + } + + private void parseIncludeKeyboardContent(final XmlPullParser parser, final boolean skip) + throws XmlPullParserException, IOException { + parseIncludeInternal(parser, null, skip); + } + + private void parseIncludeRowContent(final XmlPullParser parser, final KeyboardRow row, + final boolean skip) throws XmlPullParserException, IOException { + parseIncludeInternal(parser, row, skip); + } + + private void parseIncludeInternal(final XmlPullParser parser, final KeyboardRow row, + final boolean skip) throws XmlPullParserException, IOException { + if (skip) { + XmlParseUtils.checkEndTag(TAG_INCLUDE, parser); + if (DEBUG) startEndTag("</%s> skipped", TAG_INCLUDE); + } else { + final AttributeSet attr = Xml.asAttributeSet(parser); + final TypedArray keyboardAttr = mResources.obtainAttributes(attr, + R.styleable.Keyboard_Include); + final TypedArray keyAttr = mResources.obtainAttributes(attr, + R.styleable.Keyboard_Key); + int keyboardLayout = 0; + float savedDefaultKeyWidth = 0; + int savedDefaultKeyLabelFlags = 0; + int savedDefaultBackgroundType = Key.BACKGROUND_TYPE_NORMAL; + try { + XmlParseUtils.checkAttributeExists(keyboardAttr, + R.styleable.Keyboard_Include_keyboardLayout, "keyboardLayout", + TAG_INCLUDE, parser); + keyboardLayout = keyboardAttr.getResourceId( + R.styleable.Keyboard_Include_keyboardLayout, 0); + if (row != null) { + if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyXPos)) { + // Override current x coordinate. + row.setXPos(row.getKeyX(keyAttr)); + } + // TODO: Remove this if-clause and do the same as backgroundType below. + savedDefaultKeyWidth = row.getDefaultKeyWidth(); + if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyWidth)) { + // Override default key width. + row.setDefaultKeyWidth(row.getKeyWidth(keyAttr)); + } + savedDefaultKeyLabelFlags = row.getDefaultKeyLabelFlags(); + // Bitwise-or default keyLabelFlag if exists. + row.setDefaultKeyLabelFlags(keyAttr.getInt( + R.styleable.Keyboard_Key_keyLabelFlags, 0) + | savedDefaultKeyLabelFlags); + savedDefaultBackgroundType = row.getDefaultBackgroundType(); + // Override default backgroundType if exists. + row.setDefaultBackgroundType(keyAttr.getInt( + R.styleable.Keyboard_Key_backgroundType, + savedDefaultBackgroundType)); + } + } finally { + keyboardAttr.recycle(); + keyAttr.recycle(); + } + + XmlParseUtils.checkEndTag(TAG_INCLUDE, parser); + if (DEBUG) { + startEndTag("<%s keyboardLayout=%s />",TAG_INCLUDE, + mResources.getResourceEntryName(keyboardLayout)); + } + final XmlResourceParser parserForInclude = mResources.getXml(keyboardLayout); + try { + parseMerge(parserForInclude, row, skip); + } finally { + if (row != null) { + // Restore default keyWidth, keyLabelFlags, and backgroundType. + row.setDefaultKeyWidth(savedDefaultKeyWidth); + row.setDefaultKeyLabelFlags(savedDefaultKeyLabelFlags); + row.setDefaultBackgroundType(savedDefaultBackgroundType); + } + parserForInclude.close(); + } + } + } + + private void parseMerge(final XmlPullParser parser, final KeyboardRow row, final boolean skip) + throws XmlPullParserException, IOException { + if (DEBUG) startTag("<%s>", TAG_MERGE); + int event; + while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) { + if (event == XmlPullParser.START_TAG) { + final String tag = parser.getName(); + if (TAG_MERGE.equals(tag)) { + if (row == null) { + parseKeyboardContent(parser, skip); + } else { + parseRowContent(parser, row, skip); + } + break; + } else { + throw new XmlParseUtils.ParseException( + "Included keyboard layout must have <merge> root element", parser); + } + } + } + } + + private void parseSwitchKeyboardContent(final XmlPullParser parser, final boolean skip) + throws XmlPullParserException, IOException { + parseSwitchInternal(parser, null, skip); + } + + private void parseSwitchRowContent(final XmlPullParser parser, final KeyboardRow row, + final boolean skip) throws XmlPullParserException, IOException { + parseSwitchInternal(parser, row, skip); + } + + private void parseSwitchInternal(final XmlPullParser parser, final KeyboardRow row, + final boolean skip) throws XmlPullParserException, IOException { + if (DEBUG) startTag("<%s> %s", TAG_SWITCH, mParams.mId); + boolean selected = false; + int event; + while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) { + if (event == XmlPullParser.START_TAG) { + final String tag = parser.getName(); + if (TAG_CASE.equals(tag)) { + selected |= parseCase(parser, row, selected ? true : skip); + } else if (TAG_DEFAULT.equals(tag)) { + selected |= parseDefault(parser, row, selected ? true : skip); + } else { + throw new XmlParseUtils.IllegalStartTag(parser, TAG_KEY); + } + } else if (event == XmlPullParser.END_TAG) { + final String tag = parser.getName(); + if (TAG_SWITCH.equals(tag)) { + if (DEBUG) endTag("</%s>", TAG_SWITCH); + break; + } else { + throw new XmlParseUtils.IllegalEndTag(parser, TAG_KEY); + } + } + } + } + + private boolean parseCase(final XmlPullParser parser, final KeyboardRow row, final boolean skip) + throws XmlPullParserException, IOException { + final boolean selected = parseCaseCondition(parser); + if (row == null) { + // Processing Rows. + parseKeyboardContent(parser, selected ? skip : true); + } else { + // Processing Keys. + parseRowContent(parser, row, selected ? skip : true); + } + return selected; + } + + private boolean parseCaseCondition(final XmlPullParser parser) { + final KeyboardId id = mParams.mId; + if (id == null) { + return true; + } + final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser), + R.styleable.Keyboard_Case); + try { + final boolean keyboardLayoutSetElementMatched = matchTypedValue(a, + R.styleable.Keyboard_Case_keyboardLayoutSetElement, id.mElementId, + KeyboardId.elementIdToName(id.mElementId)); + final boolean modeMatched = matchTypedValue(a, + R.styleable.Keyboard_Case_mode, id.mMode, KeyboardId.modeName(id.mMode)); + final boolean navigateNextMatched = matchBoolean(a, + R.styleable.Keyboard_Case_navigateNext, id.navigateNext()); + final boolean navigatePreviousMatched = matchBoolean(a, + R.styleable.Keyboard_Case_navigatePrevious, id.navigatePrevious()); + final boolean passwordInputMatched = matchBoolean(a, + R.styleable.Keyboard_Case_passwordInput, id.passwordInput()); + final boolean clobberSettingsKeyMatched = matchBoolean(a, + R.styleable.Keyboard_Case_clobberSettingsKey, id.mClobberSettingsKey); + final boolean shortcutKeyEnabledMatched = matchBoolean(a, + R.styleable.Keyboard_Case_shortcutKeyEnabled, id.mShortcutKeyEnabled); + final boolean hasShortcutKeyMatched = matchBoolean(a, + R.styleable.Keyboard_Case_hasShortcutKey, id.mHasShortcutKey); + final boolean languageSwitchKeyEnabledMatched = matchBoolean(a, + R.styleable.Keyboard_Case_languageSwitchKeyEnabled, + id.mLanguageSwitchKeyEnabled); + final boolean isMultiLineMatched = matchBoolean(a, + R.styleable.Keyboard_Case_isMultiLine, id.isMultiLine()); + final boolean imeActionMatched = matchInteger(a, + R.styleable.Keyboard_Case_imeAction, id.imeAction()); + final boolean localeCodeMatched = matchString(a, + R.styleable.Keyboard_Case_localeCode, id.mLocale.toString()); + final boolean languageCodeMatched = matchString(a, + R.styleable.Keyboard_Case_languageCode, id.mLocale.getLanguage()); + final boolean countryCodeMatched = matchString(a, + R.styleable.Keyboard_Case_countryCode, id.mLocale.getCountry()); + final boolean selected = keyboardLayoutSetElementMatched && modeMatched + && navigateNextMatched && navigatePreviousMatched && passwordInputMatched + && clobberSettingsKeyMatched && shortcutKeyEnabledMatched + && hasShortcutKeyMatched && languageSwitchKeyEnabledMatched + && isMultiLineMatched && imeActionMatched && localeCodeMatched + && languageCodeMatched && countryCodeMatched; + + if (DEBUG) { + startTag("<%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s>%s", TAG_CASE, + textAttr(a.getString( + R.styleable.Keyboard_Case_keyboardLayoutSetElement), + "keyboardLayoutSetElement"), + textAttr(a.getString(R.styleable.Keyboard_Case_mode), "mode"), + textAttr(a.getString(R.styleable.Keyboard_Case_imeAction), + "imeAction"), + booleanAttr(a, R.styleable.Keyboard_Case_navigateNext, + "navigateNext"), + booleanAttr(a, R.styleable.Keyboard_Case_navigatePrevious, + "navigatePrevious"), + booleanAttr(a, R.styleable.Keyboard_Case_clobberSettingsKey, + "clobberSettingsKey"), + booleanAttr(a, R.styleable.Keyboard_Case_passwordInput, + "passwordInput"), + booleanAttr(a, R.styleable.Keyboard_Case_shortcutKeyEnabled, + "shortcutKeyEnabled"), + booleanAttr(a, R.styleable.Keyboard_Case_hasShortcutKey, + "hasShortcutKey"), + booleanAttr(a, R.styleable.Keyboard_Case_languageSwitchKeyEnabled, + "languageSwitchKeyEnabled"), + booleanAttr(a, R.styleable.Keyboard_Case_isMultiLine, + "isMultiLine"), + textAttr(a.getString(R.styleable.Keyboard_Case_localeCode), + "localeCode"), + textAttr(a.getString(R.styleable.Keyboard_Case_languageCode), + "languageCode"), + textAttr(a.getString(R.styleable.Keyboard_Case_countryCode), + "countryCode"), + selected ? "" : " skipped"); + } + + return selected; + } finally { + a.recycle(); + } + } + + private static boolean matchInteger(final TypedArray a, final int index, final int value) { + // If <case> does not have "index" attribute, that means this <case> is wild-card for + // the attribute. + return !a.hasValue(index) || a.getInt(index, 0) == value; + } + + private static boolean matchBoolean(final TypedArray a, final int index, final boolean value) { + // If <case> does not have "index" attribute, that means this <case> is wild-card for + // the attribute. + return !a.hasValue(index) || a.getBoolean(index, false) == value; + } + + private static boolean matchString(final TypedArray a, final int index, final String value) { + // If <case> does not have "index" attribute, that means this <case> is wild-card for + // the attribute. + return !a.hasValue(index) + || StringUtils.containsInArray(value, a.getString(index).split("\\|")); + } + + private static boolean matchTypedValue(final TypedArray a, final int index, final int intValue, + final String strValue) { + // If <case> does not have "index" attribute, that means this <case> is wild-card for + // the attribute. + final TypedValue v = a.peekValue(index); + if (v == null) { + return true; + } + if (ResourceUtils.isIntegerValue(v)) { + return intValue == a.getInt(index, 0); + } else if (ResourceUtils.isStringValue(v)) { + return StringUtils.containsInArray(strValue, a.getString(index).split("\\|")); + } + return false; + } + + private boolean parseDefault(final XmlPullParser parser, final KeyboardRow row, + final boolean skip) throws XmlPullParserException, IOException { + if (DEBUG) startTag("<%s>", TAG_DEFAULT); + if (row == null) { + parseKeyboardContent(parser, skip); + } else { + parseRowContent(parser, row, skip); + } + return true; + } + + private void parseKeyStyle(final XmlPullParser parser, final boolean skip) + throws XmlPullParserException, IOException { + TypedArray keyStyleAttr = mResources.obtainAttributes(Xml.asAttributeSet(parser), + R.styleable.Keyboard_KeyStyle); + TypedArray keyAttrs = mResources.obtainAttributes(Xml.asAttributeSet(parser), + R.styleable.Keyboard_Key); + try { + if (!keyStyleAttr.hasValue(R.styleable.Keyboard_KeyStyle_styleName)) { + throw new XmlParseUtils.ParseException("<" + TAG_KEY_STYLE + + "/> needs styleName attribute", parser); + } + if (DEBUG) { + startEndTag("<%s styleName=%s />%s", TAG_KEY_STYLE, + keyStyleAttr.getString(R.styleable.Keyboard_KeyStyle_styleName), + skip ? " skipped" : ""); + } + if (!skip) { + mParams.mKeyStyles.parseKeyStyleAttributes(keyStyleAttr, keyAttrs, parser); + } + } finally { + keyStyleAttr.recycle(); + keyAttrs.recycle(); + } + XmlParseUtils.checkEndTag(TAG_KEY_STYLE, parser); + } + + private void startKeyboard() { + mCurrentY += mParams.mTopPadding; + mTopEdge = true; + } + + private void startRow(final KeyboardRow row) { + addEdgeSpace(mParams.mHorizontalEdgesPadding, row); + mCurrentRow = row; + mLeftEdge = true; + mRightEdgeKey = null; + } + + private void endRow(final KeyboardRow row) { + if (mCurrentRow == null) { + throw new InflateException("orphan end row tag"); + } + if (mRightEdgeKey != null) { + mRightEdgeKey.markAsRightEdge(mParams); + mRightEdgeKey = null; + } + addEdgeSpace(mParams.mHorizontalEdgesPadding, row); + mCurrentY += row.mRowHeight; + mCurrentRow = null; + mTopEdge = false; + } + + private void endKey(final Key key) { + mParams.onAddKey(key); + if (mLeftEdge) { + key.markAsLeftEdge(mParams); + mLeftEdge = false; + } + if (mTopEdge) { + key.markAsTopEdge(mParams); + } + mRightEdgeKey = key; + } + + private void endKeyboard() { + // nothing to do here. + } + + private void addEdgeSpace(final float width, final KeyboardRow row) { + row.advanceXPos(width); + mLeftEdge = false; + mRightEdgeKey = null; + } + + private static String textAttr(final String value, final String name) { + return value != null ? String.format(" %s=%s", name, value) : ""; + } + + private static String booleanAttr(final TypedArray a, final int index, final String name) { + return a.hasValue(index) + ? String.format(" %s=%s", name, a.getBoolean(index, false)) : ""; + } +} diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java index f7981a320..f7923d0b9 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java @@ -17,13 +17,13 @@ package com.android.inputmethod.keyboard.internal; import com.android.inputmethod.keyboard.Keyboard; +import com.android.inputmethod.latin.CollectionUtils; import java.util.HashMap; public class KeyboardCodesSet { - private static final HashMap<String, int[]> sLanguageToCodesMap = - new HashMap<String, int[]>(); - private static final HashMap<String, Integer> sNameToIdMap = new HashMap<String, Integer>(); + private static final HashMap<String, int[]> sLanguageToCodesMap = CollectionUtils.newHashMap(); + private static final HashMap<String, Integer> sNameToIdMap = CollectionUtils.newHashMap(); private int[] mCodes = DEFAULT; diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java index 5155851fe..4a98a3698 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java @@ -22,6 +22,7 @@ import android.graphics.drawable.Drawable; import android.util.Log; import android.util.SparseIntArray; +import com.android.inputmethod.latin.CollectionUtils; import com.android.inputmethod.latin.R; import java.util.HashMap; @@ -35,7 +36,7 @@ public class KeyboardIconsSet { private static final SparseIntArray ATTR_ID_TO_ICON_ID = new SparseIntArray(); // Icon name to icon id map. - private static final HashMap<String, Integer> sNameToIdsMap = new HashMap<String, Integer>(); + private static final HashMap<String, Integer> sNameToIdsMap = CollectionUtils.newHashMap(); private static final Object[] NAMES_AND_ATTR_IDS = { "undefined", ATTR_UNDEFINED, diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardParams.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardParams.java new file mode 100644 index 000000000..ab5d31d42 --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardParams.java @@ -0,0 +1,137 @@ +/* + * 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.keyboard.internal; + +import android.util.SparseIntArray; + +import com.android.inputmethod.keyboard.Key; +import com.android.inputmethod.keyboard.Keyboard; +import com.android.inputmethod.keyboard.KeyboardId; +import com.android.inputmethod.latin.CollectionUtils; + +import java.util.ArrayList; +import java.util.HashSet; + +public class KeyboardParams { + public KeyboardId mId; + public int mThemeId; + + /** Total height and width of the keyboard, including the paddings and keys */ + public int mOccupiedHeight; + public int mOccupiedWidth; + + /** Base height and width of the keyboard used to calculate rows' or keys' heights and + * widths + */ + public int mBaseHeight; + public int mBaseWidth; + + public int mTopPadding; + public int mBottomPadding; + public int mHorizontalEdgesPadding; + public int mHorizontalCenterPadding; + + public KeyVisualAttributes mKeyVisualAttributes; + + public int mDefaultRowHeight; + public int mDefaultKeyWidth; + public int mHorizontalGap; + public int mVerticalGap; + + public int mMoreKeysTemplate; + public int mMaxMoreKeysKeyboardColumn; + + public int GRID_WIDTH; + public int GRID_HEIGHT; + + public final HashSet<Key> mKeys = CollectionUtils.newHashSet(); + public final ArrayList<Key> mShiftKeys = CollectionUtils.newArrayList(); + public final ArrayList<Key> mAltCodeKeysWhileTyping = CollectionUtils.newArrayList(); + public final KeyboardIconsSet mIconsSet = new KeyboardIconsSet(); + public final KeyboardCodesSet mCodesSet = new KeyboardCodesSet(); + public final KeyboardTextsSet mTextsSet = new KeyboardTextsSet(); + public final KeyStylesSet mKeyStyles = new KeyStylesSet(mTextsSet); + + public KeysCache mKeysCache; + + public int mMostCommonKeyHeight = 0; + public int mMostCommonKeyWidth = 0; + + public boolean mProximityCharsCorrectionEnabled; + + public final TouchPositionCorrection mTouchPositionCorrection = + new TouchPositionCorrection(); + + protected void clearKeys() { + mKeys.clear(); + mShiftKeys.clear(); + clearHistogram(); + } + + public void onAddKey(final Key newKey) { + final Key key = (mKeysCache != null) ? mKeysCache.get(newKey) : newKey; + final boolean zeroWidthSpacer = key.isSpacer() && key.mWidth == 0; + if (!zeroWidthSpacer) { + mKeys.add(key); + updateHistogram(key); + } + if (key.mCode == Keyboard.CODE_SHIFT) { + mShiftKeys.add(key); + } + if (key.altCodeWhileTyping()) { + mAltCodeKeysWhileTyping.add(key); + } + } + + private int mMaxHeightCount = 0; + private int mMaxWidthCount = 0; + private final SparseIntArray mHeightHistogram = new SparseIntArray(); + private final SparseIntArray mWidthHistogram = new SparseIntArray(); + + private void clearHistogram() { + mMostCommonKeyHeight = 0; + mMaxHeightCount = 0; + mHeightHistogram.clear(); + + mMaxWidthCount = 0; + mMostCommonKeyWidth = 0; + mWidthHistogram.clear(); + } + + private static int updateHistogramCounter(final SparseIntArray histogram, final int key) { + final int index = histogram.indexOfKey(key); + final int count = (index >= 0 ? histogram.get(key) : 0) + 1; + histogram.put(key, count); + return count; + } + + private void updateHistogram(final Key key) { + final int height = key.mHeight + mVerticalGap; + final int heightCount = updateHistogramCounter(mHeightHistogram, height); + if (heightCount > mMaxHeightCount) { + mMaxHeightCount = heightCount; + mMostCommonKeyHeight = height; + } + + final int width = key.mWidth + mHorizontalGap; + final int widthCount = updateHistogramCounter(mWidthHistogram, width); + if (widthCount > mMaxWidthCount) { + mMaxWidthCount = widthCount; + mMostCommonKeyWidth = width; + } + } +} diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardRow.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardRow.java new file mode 100644 index 000000000..eb17b0ea4 --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardRow.java @@ -0,0 +1,154 @@ +/* + * 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.keyboard.internal; + +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.util.Xml; + +import com.android.inputmethod.keyboard.Key; +import com.android.inputmethod.keyboard.Keyboard; +import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.ResourceUtils; + +import org.xmlpull.v1.XmlPullParser; + +/** + * Container for keys in the keyboard. All keys in a row are at the same Y-coordinate. + * Some of the key size defaults can be overridden per row from what the {@link Keyboard} + * defines. + */ +public class KeyboardRow { + // keyWidth enum constants + private static final int KEYWIDTH_NOT_ENUM = 0; + private static final int KEYWIDTH_FILL_RIGHT = -1; + + private final KeyboardParams mParams; + /** Default width of a key in this row. */ + private float mDefaultKeyWidth; + /** Default height of a key in this row. */ + public final int mRowHeight; + /** Default keyLabelFlags in this row. */ + private int mDefaultKeyLabelFlags; + /** Default backgroundType for this row */ + private int mDefaultBackgroundType; + + private final int mCurrentY; + // Will be updated by {@link Key}'s constructor. + private float mCurrentX; + + public KeyboardRow(final Resources res, final KeyboardParams params, final XmlPullParser parser, + final int y) { + mParams = params; + 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), + R.styleable.Keyboard_Key); + mDefaultKeyWidth = ResourceUtils.getDimensionOrFraction(keyAttr, + R.styleable.Keyboard_Key_keyWidth, + params.mBaseWidth, params.mDefaultKeyWidth); + mDefaultBackgroundType = keyAttr.getInt(R.styleable.Keyboard_Key_backgroundType, + Key.BACKGROUND_TYPE_NORMAL); + keyAttr.recycle(); + + // TODO: Initialize this with <Row> attribute as backgroundType is done. + mDefaultKeyLabelFlags = 0; + mCurrentY = y; + mCurrentX = 0.0f; + } + + public float getDefaultKeyWidth() { + return mDefaultKeyWidth; + } + + public void setDefaultKeyWidth(final float defaultKeyWidth) { + mDefaultKeyWidth = defaultKeyWidth; + } + + public int getDefaultKeyLabelFlags() { + return mDefaultKeyLabelFlags; + } + + public void setDefaultKeyLabelFlags(final int keyLabelFlags) { + mDefaultKeyLabelFlags = keyLabelFlags; + } + + public int getDefaultBackgroundType() { + return mDefaultBackgroundType; + } + + public void setDefaultBackgroundType(final int backgroundType) { + mDefaultBackgroundType = backgroundType; + } + + public void setXPos(final float keyXPos) { + mCurrentX = keyXPos; + } + + public void advanceXPos(final float width) { + mCurrentX += width; + } + + public int getKeyY() { + return mCurrentY; + } + + 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); + 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. + return Math.max(keyXPos + keyboardRightEdge, mCurrentX); + } else { + return keyXPos + mParams.mHorizontalEdgesPadding; + } + } + return mCurrentX; + } + + public float getKeyWidth(final TypedArray keyAttr) { + return getKeyWidth(keyAttr, mCurrentX); + } + + public float getKeyWidth(final TypedArray keyAttr, final float keyXPos) { + final int widthType = ResourceUtils.getEnumValue(keyAttr, + 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. + return keyboardRightEdge - keyXPos; + default: // KEYWIDTH_NOT_ENUM + return ResourceUtils.getDimensionOrFraction(keyAttr, + R.styleable.Keyboard_Key_keyWidth, + mParams.mBaseWidth, mDefaultKeyWidth); + } + } +} diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java index bec0f1fef..3b7c6ad7a 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java @@ -19,6 +19,7 @@ package com.android.inputmethod.keyboard.internal; import android.content.Context; import android.content.res.Resources; +import com.android.inputmethod.latin.CollectionUtils; import com.android.inputmethod.latin.R; import java.util.HashMap; @@ -45,14 +46,12 @@ import java.util.HashMap; */ public final class KeyboardTextsSet { // Language to texts map. - private static final HashMap<String, String[]> sLocaleToTextsMap = - new HashMap<String, String[]>(); - private static final HashMap<String, Integer> sNameToIdsMap = - new HashMap<String, Integer>(); + private static final HashMap<String, String[]> sLocaleToTextsMap = CollectionUtils.newHashMap(); + private static final HashMap<String, Integer> sNameToIdsMap = CollectionUtils.newHashMap(); private String[] mTexts; // Resource name to text map. - private HashMap<String, String> mResourceNameToTextsMap = new HashMap<String, String>(); + private HashMap<String, String> mResourceNameToTextsMap = CollectionUtils.newHashMap(); public void setLanguage(final String language) { mTexts = sLocaleToTextsMap.get(language); @@ -211,22 +210,29 @@ public final class KeyboardTextsSet { /* 103 */ "keylabel_for_apostrophe", /* 104 */ "keyhintlabel_for_apostrophe", /* 105 */ "more_keys_for_apostrophe", - /* 106 */ "more_keys_for_am_pm", - /* 107 */ "settings_as_more_key", - /* 108 */ "shortcut_as_more_key", - /* 109 */ "action_next_as_more_key", - /* 110 */ "action_previous_as_more_key", - /* 111 */ "label_to_more_symbol_key", - /* 112 */ "label_to_more_symbol_for_tablet_key", - /* 113 */ "label_tab_key", - /* 114 */ "label_to_phone_numeric_key", - /* 115 */ "label_to_phone_symbols_key", - /* 116 */ "label_time_am", - /* 117 */ "label_time_pm", - /* 118 */ "label_to_symbol_key_pcqwerty", - /* 119 */ "keylabel_for_popular_domain", - /* 120 */ "more_keys_for_popular_domain", - /* 121 */ "more_keys_for_smiley", + /* 106 */ "more_keys_for_q", + /* 107 */ "more_keys_for_x", + /* 108 */ "keylabel_for_q", + /* 109 */ "keylabel_for_w", + /* 110 */ "keylabel_for_y", + /* 111 */ "keylabel_for_x", + /* 112 */ "keylabel_for_spanish_row2_10", + /* 113 */ "more_keys_for_am_pm", + /* 114 */ "settings_as_more_key", + /* 115 */ "shortcut_as_more_key", + /* 116 */ "action_next_as_more_key", + /* 117 */ "action_previous_as_more_key", + /* 118 */ "label_to_more_symbol_key", + /* 119 */ "label_to_more_symbol_for_tablet_key", + /* 120 */ "label_tab_key", + /* 121 */ "label_to_phone_numeric_key", + /* 122 */ "label_to_phone_symbols_key", + /* 123 */ "label_time_am", + /* 124 */ "label_time_pm", + /* 125 */ "label_to_symbol_key_pcqwerty", + /* 126 */ "keylabel_for_popular_domain", + /* 127 */ "more_keys_for_popular_domain", + /* 128 */ "more_keys_for_smiley", }; private static final String EMPTY = ""; @@ -349,33 +355,41 @@ public final class KeyboardTextsSet { /* 103 */ "\'", /* 104 */ "\"", /* 105 */ "\"", - /* 106 */ "!fixedColumnOrder!2,!hasLabels!,!text/label_time_am,!text/label_time_pm", - /* 107 */ "!icon/settings_key|!code/key_settings", - /* 108 */ "!icon/shortcut_key|!code/key_shortcut", - /* 109 */ "!hasLabels!,!text/label_next_key|!code/key_action_next", - /* 110 */ "!hasLabels!,!text/label_previous_key|!code/key_action_previous", + /* 106 */ EMPTY, + /* 107 */ EMPTY, + /* 108 */ "q", + /* 109 */ "w", + /* 110 */ "y", + /* 111 */ "x", + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + /* 112 */ "\u00F1", + /* 113 */ "!fixedColumnOrder!2,!hasLabels!,!text/label_time_am,!text/label_time_pm", + /* 114 */ "!icon/settings_key|!code/key_settings", + /* 115 */ "!icon/shortcut_key|!code/key_shortcut", + /* 116 */ "!hasLabels!,!text/label_next_key|!code/key_action_next", + /* 117 */ "!hasLabels!,!text/label_previous_key|!code/key_action_previous", // Label for "switch to more symbol" modifier key. Must be short to fit on key! - /* 111 */ "= \\ <", + /* 118 */ "= \\ <", // Label for "switch to more symbol" modifier key on tablets. Must be short to fit on key! - /* 112 */ "~ \\ {", + /* 119 */ "~ \\ {", // Label for "Tab" key. Must be short to fit on key! - /* 113 */ "Tab", + /* 120 */ "Tab", // Label for "switch to phone numeric" key. Must be short to fit on key! - /* 114 */ "123", + /* 121 */ "123", // Label for "switch to phone symbols" key. Must be short to fit on key! // U+FF0A: "*" FULLWIDTH ASTERISK // U+FF03: "#" FULLWIDTH NUMBER SIGN - /* 115 */ "\uFF0A\uFF03", + /* 122 */ "\uFF0A\uFF03", // Key label for "ante meridiem" - /* 116 */ "AM", + /* 123 */ "AM", // Key label for "post meridiem" - /* 117 */ "PM", + /* 124 */ "PM", // Label for "switch to symbols" key on PC QWERTY layout - /* 118 */ "Sym", - /* 119 */ ".com", + /* 125 */ "Sym", + /* 126 */ ".com", // popular web domains for the locale - most popular, displayed on the keyboard - /* 120 */ "!hasLabels!,.net,.org,.gov,.edu", - /* 121 */ "!fixedColumnOrder!5,!hasLabels!,=-O|=-O ,:-P|:-P ,;-)|;-) ,:-(|:-( ,:-)|:-) ,:-!|:-! ,:-$|:-$ ,B-)|B-) ,:O|:O ,:-*|:-* ,:-D|:-D ,:\'(|:\'( ,:-\\\\|:-\\\\ ,O:-)|O:-) ,:-[|:-[ ", + /* 127 */ "!hasLabels!,.net,.org,.gov,.edu", + /* 128 */ "!fixedColumnOrder!5,!hasLabels!,=-O|=-O ,:-P|:-P ,;-)|;-) ,:-(|:-( ,:-)|:-) ,:-!|:-! ,:-$|:-$ ,B-)|B-) ,:O|:O ,:-*|:-* ,:-D|:-D ,:\'(|:\'( ,:-\\\\|:-\\\\ ,O:-)|O:-) ,:-[|:-[ ", }; /* Language af: Afrikaans */ @@ -858,6 +872,144 @@ public final class KeyboardTextsSet { /* 7 */ "\u00E7", }; + /* Language eo: Esperanto */ + private static final String[] LANGUAGE_eo = { + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + // U+0103: "ă" LATIN SMALL LETTER A WITH BREVE + // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK + // U+00AA: "ª" FEMININE ORDINAL INDICATOR + /* 0 */ "\u00E1,\u00E0,\u00E2,\u00E4,\u00E6,\u00E3,\u00E5,\u0101,\u0103,\u0105,\u00AA", + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+011B: "ě" LATIN SMALL LETTER E WITH CARON + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK + // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE + // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON + /* 1 */ "\u00E9,\u011B,\u00E8,\u00EA,\u00EB,\u0119,\u0117,\u0113", + // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE + // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX + // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS + // U+0129: "ĩ" LATIN SMALL LETTER I WITH TILDE + // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE + // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK + // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON + // U+0131: "ı" LATIN SMALL LETTER DOTLESS I + // U+0133: "ij" LATIN SMALL LIGATURE IJ + /* 2 */ "\u00ED,\u00EE,\u00EF,\u0129,\u00EC,\u012F,\u012B,\u0131,\u0133", + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+0153: "œ" LATIN SMALL LIGATURE OE + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON + // U+0151: "ő" LATIN SMALL LETTER O WITH DOUBLE ACUTE + // U+00BA: "º" MASCULINE ORDINAL INDICATOR + /* 3 */ "\u00F3,\u00F6,\u00F4,\u00F2,\u00F5,\u0153,\u00F8,\u014D,\u0151,\u00BA", + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+016F: "ů" LATIN SMALL LETTER U WITH RING ABOVE + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + // U+0169: "ũ" LATIN SMALL LETTER U WITH TILDE + // U+0171: "ű" LATIN SMALL LETTER U WITH DOUBLE ACUTE + // U+0173: "ų" LATIN SMALL LETTER U WITH OGONEK + // U+00B5: "µ" MICRO SIGN + /* 4 */ "\u00FA,\u016F,\u00FB,\u00FC,\u00F9,\u016B,\u0169,\u0171,\u0173,\u00B5", + // U+00DF: "ß" LATIN SMALL LETTER SHARP S + // U+0161: "š" LATIN SMALL LETTER S WITH CARON + // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE + // U+0219: "ș" LATIN SMALL LETTER S WITH COMMA BELOW + // U+015F: "ş" LATIN SMALL LETTER S WITH CEDILLA + /* 5 */ "\u00DF,\u0161,\u015B,\u0219,\u015F", + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE + // U+0146: "ņ" LATIN SMALL LETTER N WITH CEDILLA + // U+0148: "ň" LATIN SMALL LETTER N WITH CARON + // U+0149: "ʼn" LATIN SMALL LETTER N PRECEDED BY APOSTROPHE + // U+014B: "ŋ" LATIN SMALL LETTER ENG + /* 6 */ "\u00F1,\u0144,\u0146,\u0148,\u0149,\u014B", + // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE + // U+010D: "č" LATIN SMALL LETTER C WITH CARON + // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA + // U+010B: "ċ" LATIN SMALL LETTER C WITH DOT ABOVE + /* 7 */ "\u0107,\u010D,\u00E7,\u010B", + // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE + // U+0177: "ŷ" LATIN SMALL LETTER Y WITH CIRCUMFLEX + // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS + // U+00FE: "þ" LATIN SMALL LETTER THORN + /* 8 */ "y,\u00FD,\u0177,\u00FF,\u00FE", + // U+00F0: "ð" LATIN SMALL LETTER ETH + // U+010F: "ď" LATIN SMALL LETTER D WITH CARON + // U+0111: "đ" LATIN SMALL LETTER D WITH STROKE + /* 9 */ "\u00F0,\u010F,\u0111", + // U+0159: "ř" LATIN SMALL LETTER R WITH CARON + // U+0155: "ŕ" LATIN SMALL LETTER R WITH ACUTE + // U+0157: "ŗ" LATIN SMALL LETTER R WITH CEDILLA + /* 10 */ "\u0159,\u0155,\u0157", + // U+0165: "ť" LATIN SMALL LETTER T WITH CARON + // U+021B: "ț" LATIN SMALL LETTER T WITH COMMA BELOW + // U+0163: "ţ" LATIN SMALL LETTER T WITH CEDILLA + // U+0167: "ŧ" LATIN SMALL LETTER T WITH STROKE + /* 11 */ "\u0165,\u021B,\u0163,\u0167", + // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE + // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE + // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON + /* 12 */ "\u017A,\u017C,\u017E", + // U+0137: "ķ" LATIN SMALL LETTER K WITH CEDILLA + // U+0138: "ĸ" LATIN SMALL LETTER KRA + /* 13 */ "\u0137,\u0138", + // U+013A: "ĺ" LATIN SMALL LETTER L WITH ACUTE + // U+013C: "ļ" LATIN SMALL LETTER L WITH CEDILLA + // U+013E: "ľ" LATIN SMALL LETTER L WITH CARON + // U+0140: "ŀ" LATIN SMALL LETTER L WITH MIDDLE DOT + // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE + /* 14 */ "\u013A,\u013C,\u013E,\u0140,\u0142", + // U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE + // U+0121: "ġ" LATIN SMALL LETTER G WITH DOT ABOVE + // U+0123: "ģ" LATIN SMALL LETTER G WITH CEDILLA + /* 15 */ "\u011F,\u0121,\u0123", + // U+0175: "ŵ" LATIN SMALL LETTER W WITH CIRCUMFLEX + /* 16 */ "w,\u0175", + // U+0125: "ĥ" LATIN SMALL LETTER H WITH CIRCUMFLEX + // U+0127: "ħ" LATIN SMALL LETTER H WITH STROKE + /* 17 */ "\u0125,\u0127", + /* 18 */ null, + // U+0175: "ŵ" LATIN SMALL LETTER W WITH CIRCUMFLEX + /* 19 */ "w,\u0175", + /* 20~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, + /* ~105 */ + /* 106 */ "q", + /* 107 */ "x", + // U+015D: "ŝ" LATIN SMALL LETTER S WITH CIRCUMFLEX + /* 108 */ "\u015D", + // U+011D: "ĝ" LATIN SMALL LETTER G WITH CIRCUMFLEX + /* 109 */ "\u011D", + // U+016D: "ŭ" LATIN SMALL LETTER U WITH BREVE + /* 110 */ "\u016D", + // U+0109: "ĉ" LATIN SMALL LETTER C WITH CIRCUMFLEX + /* 111 */ "\u0109", + // U+0135: "ĵ" LATIN SMALL LETTER J WITH CIRCUMFLEX + /* 112 */ "\u0135", + }; + /* Language es: Spanish */ private static final String[] LANGUAGE_es = { // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE @@ -2696,6 +2848,7 @@ public final class KeyboardTextsSet { "da", LANGUAGE_da, /* Danish */ "de", LANGUAGE_de, /* German */ "en", LANGUAGE_en, /* English */ + "eo", LANGUAGE_eo, /* Esperanto */ "es", LANGUAGE_es, /* Spanish */ "et", LANGUAGE_et, /* Estonian */ "fa", LANGUAGE_fa, /* Persian */ diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeysCache.java b/java/src/com/android/inputmethod/keyboard/internal/KeysCache.java new file mode 100644 index 000000000..f54617c98 --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/internal/KeysCache.java @@ -0,0 +1,40 @@ +/* + * 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.keyboard.internal; + +import com.android.inputmethod.keyboard.Key; +import com.android.inputmethod.latin.CollectionUtils; + +import java.util.HashMap; + +public class KeysCache { + private final HashMap<Key, Key> mMap = CollectionUtils.newHashMap(); + + public void clear() { + mMap.clear(); + } + + public Key get(final Key key) { + final Key existingKey = mMap.get(key); + if (existingKey != null) { + // Reuse the existing element that equals to "key" without adding "key" to the map. + return existingKey; + } + mMap.put(key, key); + return key; + } +} diff --git a/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java b/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java new file mode 100644 index 000000000..5da26543f --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java @@ -0,0 +1,61 @@ +/* + * 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.keyboard.internal; + +import com.android.inputmethod.keyboard.Keyboard; +import com.android.inputmethod.latin.StringUtils; + +import java.util.Locale; + +public class MoreKeySpec { + public final int mCode; + public final String mLabel; + public final String mOutputText; + public final int mIconId; + + public MoreKeySpec(final String moreKeySpec, boolean needsToUpperCase, final Locale locale, + final KeyboardCodesSet codesSet) { + mLabel = KeySpecParser.toUpperCaseOfStringForLocale( + KeySpecParser.getLabel(moreKeySpec), needsToUpperCase, locale); + final int code = KeySpecParser.toUpperCaseOfCodeForLocale( + KeySpecParser.getCode(moreKeySpec, codesSet), needsToUpperCase, locale); + if (code == Keyboard.CODE_UNSPECIFIED) { + // Some letter, for example German Eszett (U+00DF: "ß"), has multiple characters + // upper case representation ("SS"). + mCode = Keyboard.CODE_OUTPUT_TEXT; + mOutputText = mLabel; + } else { + mCode = code; + mOutputText = KeySpecParser.toUpperCaseOfStringForLocale( + KeySpecParser.getOutputText(moreKeySpec), needsToUpperCase, locale); + } + mIconId = KeySpecParser.getIconId(moreKeySpec); + } + + @Override + public String toString() { + final String label = (mIconId == KeyboardIconsSet.ICON_UNDEFINED ? mLabel + : KeySpecParser.PREFIX_ICON + KeyboardIconsSet.getIconName(mIconId)); + final String output = (mCode == Keyboard.CODE_OUTPUT_TEXT ? mOutputText + : Keyboard.printableCode(mCode)); + if (StringUtils.codePointCount(label) == 1 && label.codePointAt(0) == mCode) { + return output; + } else { + return label + "|" + output; + } + } +} diff --git a/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java b/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java index 1c7ceaf92..e0858c019 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java +++ b/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java @@ -18,6 +18,8 @@ package com.android.inputmethod.keyboard.internal; import android.util.Log; +import com.android.inputmethod.latin.CollectionUtils; + import java.util.ArrayList; public class PointerTrackerQueue { @@ -32,7 +34,7 @@ public class PointerTrackerQueue { private static final int INITIAL_CAPACITY = 10; private final ArrayList<Element> mExpandableArrayOfActivePointers = - new ArrayList<Element>(INITIAL_CAPACITY); + CollectionUtils.newArrayList(INITIAL_CAPACITY); private int mArraySize = 0; public synchronized int size() { diff --git a/java/src/com/android/inputmethod/keyboard/internal/PreviewPlacerView.java b/java/src/com/android/inputmethod/keyboard/internal/PreviewPlacerView.java index d0fecf060..269b202b5 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/PreviewPlacerView.java +++ b/java/src/com/android/inputmethod/keyboard/internal/PreviewPlacerView.java @@ -23,10 +23,13 @@ import android.graphics.Paint; import android.graphics.Paint.Align; import android.os.Message; import android.text.TextUtils; +import android.util.AttributeSet; import android.util.SparseArray; import android.widget.RelativeLayout; import com.android.inputmethod.keyboard.PointerTracker; +import com.android.inputmethod.keyboard.internal.GesturePreviewTrail.GesturePreviewTrailParams; +import com.android.inputmethod.latin.CollectionUtils; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.StaticInnerHandlerWrapper; @@ -46,29 +49,42 @@ public class PreviewPlacerView extends RelativeLayout { private int mXOrigin; private int mYOrigin; - private final SparseArray<PointerTracker> mPointers = new SparseArray<PointerTracker>(); + private final SparseArray<GesturePreviewTrail> mGesturePreviewTrails = + CollectionUtils.newSparseArray(); + private final GesturePreviewTrailParams mGesturePreviewTrailParams; private String mGestureFloatingPreviewText; + private int mLastPointerX; + private int mLastPointerY; + private boolean mDrawsGesturePreviewTrail; private boolean mDrawsGestureFloatingPreviewText; - private final DrawingHandler mDrawingHandler = new DrawingHandler(this); + private final DrawingHandler mDrawingHandler; private static class DrawingHandler extends StaticInnerHandlerWrapper<PreviewPlacerView> { private static final int MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT = 0; + private static final int MSG_UPDATE_GESTURE_PREVIEW_TRAIL = 1; + + private final GesturePreviewTrailParams mGesturePreviewTrailParams; - public DrawingHandler(PreviewPlacerView outerInstance) { + public DrawingHandler(final PreviewPlacerView outerInstance, + final GesturePreviewTrailParams gesturePreviewTrailParams) { super(outerInstance); + mGesturePreviewTrailParams = gesturePreviewTrailParams; } @Override - public void handleMessage(Message msg) { + public void handleMessage(final Message msg) { final PreviewPlacerView placerView = getOuterInstance(); if (placerView == null) return; switch (msg.what) { case MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT: placerView.setGestureFloatingPreviewText(null); break; + case MSG_UPDATE_GESTURE_PREVIEW_TRAIL: + placerView.invalidate(); + break; } } @@ -84,15 +100,32 @@ public class PreviewPlacerView extends RelativeLayout { placerView.mGestureFloatingPreviewTextLingerTimeout); } + private void cancelUpdateGestureTrailPreview() { + removeMessages(MSG_UPDATE_GESTURE_PREVIEW_TRAIL); + } + + public void postUpdateGestureTrailPreview() { + cancelUpdateGestureTrailPreview(); + sendMessageDelayed(obtainMessage(MSG_UPDATE_GESTURE_PREVIEW_TRAIL), + mGesturePreviewTrailParams.mUpdateInterval); + } + public void cancelAllMessages() { cancelDismissGestureFloatingPreviewText(); + cancelUpdateGestureTrailPreview(); } } - public PreviewPlacerView(Context context, TypedArray keyboardViewAttr) { + public PreviewPlacerView(final Context context, final AttributeSet attrs) { + this(context, attrs, R.attr.keyboardViewStyle); + } + + public PreviewPlacerView(final Context context, final AttributeSet attrs, final int defStyle) { super(context); setWillNotDraw(false); + final TypedArray keyboardViewAttr = context.obtainStyledAttributes( + attrs, R.styleable.KeyboardView, defStyle, R.style.KeyboardView); final int gestureFloatingPreviewTextSize = keyboardViewAttr.getDimensionPixelSize( R.styleable.KeyboardView_gestureFloatingPreviewTextSize, 0); mGestureFloatingPreviewTextColor = keyboardViewAttr.getColor( @@ -117,6 +150,10 @@ public class PreviewPlacerView extends RelativeLayout { R.styleable.KeyboardView_gesturePreviewTrailColor, 0); final int gesturePreviewTrailWidth = keyboardViewAttr.getDimensionPixelSize( R.styleable.KeyboardView_gesturePreviewTrailWidth, 0); + mGesturePreviewTrailParams = new GesturePreviewTrailParams(keyboardViewAttr); + keyboardViewAttr.recycle(); + + mDrawingHandler = new DrawingHandler(this, mGesturePreviewTrailParams); mGesturePaint = new Paint(); mGesturePaint.setAntiAlias(true); @@ -132,48 +169,60 @@ public class PreviewPlacerView extends RelativeLayout { mTextPaint.setTextSize(gestureFloatingPreviewTextSize); } - public void setOrigin(int x, int y) { + public void setOrigin(final int x, final int y) { mXOrigin = x; mYOrigin = y; } - public void setGesturePreviewMode(boolean drawsGesturePreviewTrail, - boolean drawsGestureFloatingPreviewText) { + public void setGesturePreviewMode(final boolean drawsGesturePreviewTrail, + final boolean drawsGestureFloatingPreviewText) { mDrawsGesturePreviewTrail = drawsGesturePreviewTrail; mDrawsGestureFloatingPreviewText = drawsGestureFloatingPreviewText; } - public void invalidatePointer(PointerTracker tracker) { - synchronized (mPointers) { - mPointers.put(tracker.mPointerId, tracker); - // TODO: Should narrow the invalidate region. - invalidate(); + public void invalidatePointer(final PointerTracker tracker) { + GesturePreviewTrail trail; + synchronized (mGesturePreviewTrails) { + trail = mGesturePreviewTrails.get(tracker.mPointerId); + if (trail == null) { + trail = new GesturePreviewTrail(mGesturePreviewTrailParams); + mGesturePreviewTrails.put(tracker.mPointerId, trail); + } } + trail.addStroke(tracker.getGestureStrokeWithPreviewTrail(), tracker.getDownTime()); + + mLastPointerX = tracker.getLastX(); + mLastPointerY = tracker.getLastY(); + // TODO: Should narrow the invalidate region. + invalidate(); } @Override - public void onDraw(Canvas canvas) { + public void onDraw(final Canvas canvas) { super.onDraw(canvas); - synchronized (mPointers) { - canvas.translate(mXOrigin, mYOrigin); - final int trackerCount = mPointers.size(); - boolean hasDrawnFloatingPreviewText = false; - for (int index = 0; index < trackerCount; index++) { - final PointerTracker tracker = mPointers.valueAt(index); - if (mDrawsGesturePreviewTrail) { - tracker.drawGestureTrail(canvas, mGesturePaint); - } - // TODO: Figure out more cleaner way to draw gesture preview text. - if (mDrawsGestureFloatingPreviewText && !hasDrawnFloatingPreviewText) { - drawGestureFloatingPreviewText(canvas, tracker, mGestureFloatingPreviewText); - hasDrawnFloatingPreviewText = true; + canvas.translate(mXOrigin, mYOrigin); + if (mDrawsGesturePreviewTrail) { + boolean needsUpdatingGesturePreviewTrail = false; + synchronized (mGesturePreviewTrails) { + // Trails count == fingers count that have ever been active. + final int trailsCount = mGesturePreviewTrails.size(); + for (int index = 0; index < trailsCount; index++) { + final GesturePreviewTrail trail = mGesturePreviewTrails.valueAt(index); + needsUpdatingGesturePreviewTrail |= + trail.drawGestureTrail(canvas, mGesturePaint); } } - canvas.translate(-mXOrigin, -mYOrigin); + if (needsUpdatingGesturePreviewTrail) { + mDrawingHandler.postUpdateGestureTrailPreview(); + } + } + if (mDrawsGestureFloatingPreviewText) { + drawGestureFloatingPreviewText(canvas, mGestureFloatingPreviewText); } + canvas.translate(-mXOrigin, -mYOrigin); } - public void setGestureFloatingPreviewText(String gestureFloatingPreviewText) { + public void setGestureFloatingPreviewText(final String gestureFloatingPreviewText) { mGestureFloatingPreviewText = gestureFloatingPreviewText; invalidate(); } @@ -186,15 +235,17 @@ public class PreviewPlacerView extends RelativeLayout { mDrawingHandler.cancelAllMessages(); } - private void drawGestureFloatingPreviewText(Canvas canvas, PointerTracker tracker, - String gestureFloatingPreviewText) { + private void drawGestureFloatingPreviewText(final Canvas canvas, + final String gestureFloatingPreviewText) { if (TextUtils.isEmpty(gestureFloatingPreviewText)) { return; } final Paint paint = mTextPaint; - final int lastX = tracker.getLastX(); - final int lastY = tracker.getLastY(); + // TODO: Figure out how we should deal with the floating preview text with multiple moving + // fingers. + final int lastX = mLastPointerX; + final int lastY = mLastPointerY; final int textSize = (int)paint.getTextSize(); final int canvasWidth = canvas.getWidth(); diff --git a/java/src/com/android/inputmethod/keyboard/internal/SuddenJumpingTouchEventHandler.java b/java/src/com/android/inputmethod/keyboard/internal/SuddenJumpingTouchEventHandler.java index 9e2cbec52..a591a7ac3 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/SuddenJumpingTouchEventHandler.java +++ b/java/src/com/android/inputmethod/keyboard/internal/SuddenJumpingTouchEventHandler.java @@ -24,7 +24,7 @@ import com.android.inputmethod.keyboard.Keyboard; import com.android.inputmethod.keyboard.MainKeyboardView; import com.android.inputmethod.latin.LatinImeLogger; import com.android.inputmethod.latin.R; -import com.android.inputmethod.latin.Utils; +import com.android.inputmethod.latin.ResourceUtils; import com.android.inputmethod.latin.define.ProductionFlag; import com.android.inputmethod.research.ResearchLogger; @@ -53,7 +53,7 @@ public class SuddenJumpingTouchEventHandler { public SuddenJumpingTouchEventHandler(Context context, ProcessMotionEvent view) { mView = view; - mNeedsSuddenJumpingHack = Boolean.parseBoolean(Utils.getDeviceOverrideValue( + mNeedsSuddenJumpingHack = Boolean.parseBoolean(ResourceUtils.getDeviceOverrideValue( context.getResources(), R.array.sudden_jumping_touch_event_device_list, "false")); } diff --git a/java/src/com/android/inputmethod/keyboard/internal/TouchPositionCorrection.java b/java/src/com/android/inputmethod/keyboard/internal/TouchPositionCorrection.java new file mode 100644 index 000000000..69dc01cd6 --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/internal/TouchPositionCorrection.java @@ -0,0 +1,76 @@ +/* + * 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.keyboard.internal; + +import com.android.inputmethod.latin.LatinImeLogger; + +public class TouchPositionCorrection { + private static final int TOUCH_POSITION_CORRECTION_RECORD_SIZE = 3; + + public boolean mEnabled; + public float[] mXs; + public float[] mYs; + public float[] mRadii; + + public void load(final String[] data) { + final int dataLength = data.length; + if (dataLength % TOUCH_POSITION_CORRECTION_RECORD_SIZE != 0) { + if (LatinImeLogger.sDBG) { + throw new RuntimeException( + "the size of touch position correction data is invalid"); + } + return; + } + + final int length = dataLength / TOUCH_POSITION_CORRECTION_RECORD_SIZE; + mXs = new float[length]; + mYs = new float[length]; + mRadii = new float[length]; + try { + for (int i = 0; i < dataLength; ++i) { + final int type = i % TOUCH_POSITION_CORRECTION_RECORD_SIZE; + final int index = i / TOUCH_POSITION_CORRECTION_RECORD_SIZE; + final float value = Float.parseFloat(data[i]); + if (type == 0) { + mXs[index] = value; + } else if (type == 1) { + mYs[index] = value; + } else { + mRadii[index] = value; + } + } + } catch (NumberFormatException e) { + if (LatinImeLogger.sDBG) { + throw new RuntimeException( + "the number format for touch position correction data is invalid"); + } + mXs = null; + mYs = null; + mRadii = null; + } + } + + // TODO: Remove this method. + public void setEnabled(final boolean enabled) { + mEnabled = enabled; + } + + public boolean isValid() { + return mEnabled && mXs != null && mYs != null && mRadii != null + && mXs.length > 0 && mYs.length > 0 && mRadii.length > 0; + } +} diff --git a/java/src/com/android/inputmethod/latin/AdditionalSubtype.java b/java/src/com/android/inputmethod/latin/AdditionalSubtype.java index f8f1395b3..4b47a261f 100644 --- a/java/src/com/android/inputmethod/latin/AdditionalSubtype.java +++ b/java/src/com/android/inputmethod/latin/AdditionalSubtype.java @@ -91,7 +91,7 @@ public class AdditionalSubtype { } final String[] prefSubtypeArray = prefSubtypes.split(PREF_SUBTYPE_SEPARATOR); final ArrayList<InputMethodSubtype> subtypesList = - new ArrayList<InputMethodSubtype>(prefSubtypeArray.length); + CollectionUtils.newArrayList(prefSubtypeArray.length); for (final String prefSubtype : prefSubtypeArray) { final InputMethodSubtype subtype = createAdditionalSubtype(prefSubtype); if (subtype.getNameResId() == SubtypeLocale.UNKNOWN_KEYBOARD_LAYOUT) { diff --git a/java/src/com/android/inputmethod/latin/AdditionalSubtypeSettings.java b/java/src/com/android/inputmethod/latin/AdditionalSubtypeSettings.java index 779a38823..d01592a4d 100644 --- a/java/src/com/android/inputmethod/latin/AdditionalSubtypeSettings.java +++ b/java/src/com/android/inputmethod/latin/AdditionalSubtypeSettings.java @@ -89,7 +89,7 @@ public class AdditionalSubtypeSettings extends PreferenceFragment { super(context, android.R.layout.simple_spinner_item); setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - final TreeSet<SubtypeLocaleItem> items = new TreeSet<SubtypeLocaleItem>(); + final TreeSet<SubtypeLocaleItem> items = CollectionUtils.newTreeSet(); final InputMethodInfo imi = ImfUtils.getInputMethodInfoOfThisIme(context); final int count = imi.getSubtypeCount(); for (int i = 0; i < count; i++) { @@ -533,7 +533,7 @@ public class AdditionalSubtypeSettings extends PreferenceFragment { private InputMethodSubtype[] getSubtypes() { final PreferenceGroup group = getPreferenceScreen(); - final ArrayList<InputMethodSubtype> subtypes = new ArrayList<InputMethodSubtype>(); + final ArrayList<InputMethodSubtype> subtypes = CollectionUtils.newArrayList(); final int count = group.getPreferenceCount(); for (int i = 0; i < count; i++) { final Preference pref = group.getPreference(i); diff --git a/java/src/com/android/inputmethod/latin/AutoCorrection.java b/java/src/com/android/inputmethod/latin/AutoCorrection.java index 048166807..01ba30077 100644 --- a/java/src/com/android/inputmethod/latin/AutoCorrection.java +++ b/java/src/com/android/inputmethod/latin/AutoCorrection.java @@ -39,7 +39,6 @@ public class AutoCorrection { } final CharSequence lowerCasedWord = word.toString().toLowerCase(); for (final String key : dictionaries.keySet()) { - if (key.equals(Dictionary.TYPE_WHITELIST)) continue; final Dictionary dictionary = dictionaries.get(key); // It's unclear how realistically 'dictionary' can be null, but the monkey is somehow // managing to get null in here. Presumably the language is changing to a language with @@ -64,7 +63,6 @@ public class AutoCorrection { } int maxFreq = -1; for (final String key : dictionaries.keySet()) { - if (key.equals(Dictionary.TYPE_WHITELIST)) continue; final Dictionary dictionary = dictionaries.get(key); if (null == dictionary) continue; final int tempFreq = dictionary.getFrequency(word); diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java index f0f5cd320..8909526d8 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java @@ -18,6 +18,7 @@ package com.android.inputmethod.latin; import android.content.Context; import android.text.TextUtils; +import android.util.SparseArray; import com.android.inputmethod.keyboard.ProximityInfo; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; @@ -51,6 +52,7 @@ public class BinaryDictionary extends Dictionary { private static final int TYPED_LETTER_MULTIPLIER = 2; private long mNativeDict; + private final Locale mLocale; private final int[] mInputCodePoints = new int[MAX_WORD_LENGTH]; // TODO: The below should be int[] mOutputCodePoints private final char[] mOutputChars = new char[MAX_WORD_LENGTH * MAX_RESULTS]; @@ -59,7 +61,25 @@ public class BinaryDictionary extends Dictionary { private final int[] mOutputTypes = new int[MAX_RESULTS]; private final boolean mUseFullEditDistance; - private final DicTraverseSession mDicTraverseSession; + + private final SparseArray<DicTraverseSession> mDicTraverseSessions = + CollectionUtils.newSparseArray(); + + // TODO: There should be a way to remove used DicTraverseSession objects from + // {@code mDicTraverseSessions}. + private DicTraverseSession getTraverseSession(int traverseSessionId) { + synchronized(mDicTraverseSessions) { + DicTraverseSession traverseSession = mDicTraverseSessions.get(traverseSessionId); + if (traverseSession == null) { + traverseSession = mDicTraverseSessions.get(traverseSessionId); + if (traverseSession == null) { + traverseSession = new DicTraverseSession(mLocale, mNativeDict); + mDicTraverseSessions.put(traverseSessionId, traverseSession); + } + } + return traverseSession; + } + } /** * Constructor for the binary dictionary. This is supposed to be called from the @@ -76,10 +96,9 @@ public class BinaryDictionary extends Dictionary { final String filename, final long offset, final long length, final boolean useFullEditDistance, final Locale locale, final String dictType) { super(dictType); + mLocale = locale; mUseFullEditDistance = useFullEditDistance; loadDictionary(filename, offset, length); - mDicTraverseSession = new DicTraverseSession(locale); - mDicTraverseSession.initSession(mNativeDict); } static { @@ -109,8 +128,15 @@ public class BinaryDictionary extends Dictionary { @Override public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer, final CharSequence prevWord, final ProximityInfo proximityInfo) { + return getSuggestionsWithSessionId(composer, prevWord, proximityInfo, 0); + } + + @Override + public ArrayList<SuggestedWordInfo> getSuggestionsWithSessionId(final WordComposer composer, + final CharSequence prevWord, final ProximityInfo proximityInfo, int sessionId) { if (!isValidDictionary()) return null; - Arrays.fill(mInputCodePoints, WordComposer.NOT_A_CODE); + + Arrays.fill(mInputCodePoints, Constants.NOT_A_CODE); // TODO: toLowerCase in the native code final int[] prevWordCodePointArray = (null == prevWord) ? null : StringUtils.toCodePointArray(prevWord.toString()); @@ -128,18 +154,18 @@ public class BinaryDictionary extends Dictionary { final int codesSize = isGesture ? ips.getPointerSize() : composerSize; // proximityInfo and/or prevWordForBigrams may not be null. final int tmpCount = getSuggestionsNative(mNativeDict, - proximityInfo.getNativeProximityInfo(), mDicTraverseSession.getSession(), + proximityInfo.getNativeProximityInfo(), getTraverseSession(sessionId).getSession(), ips.getXCoordinates(), ips.getYCoordinates(), ips.getTimes(), ips.getPointerIds(), mInputCodePoints, codesSize, 0 /* commitPoint */, isGesture, prevWordCodePointArray, mUseFullEditDistance, mOutputChars, mOutputScores, mSpaceIndices, mOutputTypes); final int count = Math.min(tmpCount, MAX_PREDICTIONS); - final ArrayList<SuggestedWordInfo> suggestions = new ArrayList<SuggestedWordInfo>(); + final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList(); for (int j = 0; j < count; ++j) { if (composerSize > 0 && mOutputScores[j] < 1) break; final int start = j * MAX_WORD_LENGTH; int len = 0; - while (len < MAX_WORD_LENGTH && mOutputChars[start + len] != 0) { + while (len < MAX_WORD_LENGTH && mOutputChars[start + len] != 0) { ++len; } if (len > 0) { @@ -186,12 +212,20 @@ public class BinaryDictionary extends Dictionary { } @Override - public synchronized void close() { - mDicTraverseSession.close(); + public void close() { + synchronized (mDicTraverseSessions) { + final int sessionsSize = mDicTraverseSessions.size(); + for (int index = 0; index < sessionsSize; ++index) { + final DicTraverseSession traverseSession = mDicTraverseSessions.valueAt(index); + if (traverseSession != null) { + traverseSession.close(); + } + } + } closeInternal(); } - private void closeInternal() { + private synchronized void closeInternal() { if (mNativeDict != 0) { closeNative(mNativeDict); mNativeDict = 0; diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java index 236c198ad..799aea8ef 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java @@ -99,7 +99,7 @@ public class BinaryDictionaryFileDumper { } try { - final List<WordListInfo> list = new ArrayList<WordListInfo>(); + final List<WordListInfo> list = CollectionUtils.newArrayList(); do { final String wordListId = c.getString(0); final String wordListLocale = c.getString(1); @@ -267,7 +267,7 @@ public class BinaryDictionaryFileDumper { final ContentResolver resolver = context.getContentResolver(); final List<WordListInfo> idList = getWordListWordListInfos(locale, context, hasDefaultWordList); - final List<AssetFileAddress> fileAddressList = new ArrayList<AssetFileAddress>(); + final List<AssetFileAddress> fileAddressList = CollectionUtils.newArrayList(); for (WordListInfo id : idList) { final AssetFileAddress afd = cacheWordList(id.mId, id.mLocale, resolver, context); if (null != afd) { diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java index 063243e1b..e1cb195bc 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java @@ -16,6 +16,8 @@ package com.android.inputmethod.latin; +import com.android.inputmethod.latin.makedict.BinaryDictInputOutput; + import android.content.Context; import android.content.SharedPreferences; import android.content.pm.PackageManager.NameNotFoundException; @@ -23,6 +25,10 @@ import android.content.res.AssetFileDescriptor; import android.util.Log; import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; import java.util.ArrayList; import java.util.HashMap; import java.util.Locale; @@ -51,6 +57,9 @@ class BinaryDictionaryGetter { private static final String MAIN_DICTIONARY_CATEGORY = "main"; public static final String ID_CATEGORY_SEPARATOR = ":"; + // The key considered to read the version attribute in a dictionary file. + private static String VERSION_KEY = "version"; + // Prevents this from being instantiated private BinaryDictionaryGetter() {} @@ -254,8 +263,7 @@ class BinaryDictionaryGetter { final Context context) { final File[] directoryList = getCachedDirectoryList(context); if (null == directoryList) return EMPTY_FILE_ARRAY; - final HashMap<String, FileAndMatchLevel> cacheFiles = - new HashMap<String, FileAndMatchLevel>(); + final HashMap<String, FileAndMatchLevel> cacheFiles = CollectionUtils.newHashMap(); for (File directory : directoryList) { if (!directory.isDirectory()) continue; final String dirLocale = getWordListIdFromFileName(directory.getName()); @@ -336,6 +344,54 @@ class BinaryDictionaryGetter { 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. + private static boolean hackCanUseDictionaryFile(final Locale locale, final File f) { + // Only for English - other languages didn't have a whitelist, hence this + // ad-hock ## HACK ## + if (!Locale.ENGLISH.getLanguage().equals(locale.getLanguage())) return true; + + FileInputStream inStream = null; + try { + // Read the version of the file + inStream = new FileInputStream(f); + final ByteBuffer buffer = inStream.getChannel().map( + FileChannel.MapMode.READ_ONLY, 0, f.length()); + final int magic = buffer.getInt(); + if (magic != BinaryDictInputOutput.VERSION_2_MAGIC_NUMBER) { + return false; + } + final int formatVersion = buffer.getInt(); + final int headerSize = buffer.getInt(); + final HashMap<String, String> options = CollectionUtils.newHashMap(); + BinaryDictInputOutput.populateOptions(buffer, headerSize, options); + + final String version = options.get(VERSION_KEY); + if (null == version) { + // No version in the options : the format is unexpected + return false; + } + // Version 18 is the first one to include the whitelist + // Obviously this is a big ## HACK ## + return Integer.parseInt(version) >= 18; + } catch (java.io.FileNotFoundException e) { + return false; + } catch (java.io.IOException e) { + return false; + } catch (NumberFormatException e) { + return false; + } finally { + if (inStream != null) { + try { + inStream.close(); + } catch (IOException e) { + // do nothing + } + } + } + } + /** * Returns a list of file addresses for a given locale, trying relevant methods in order. * @@ -362,18 +418,19 @@ class BinaryDictionaryGetter { final DictPackSettings dictPackSettings = new DictPackSettings(context); boolean foundMainDict = false; - final ArrayList<AssetFileAddress> fileList = new ArrayList<AssetFileAddress>(); + 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()); - if (isMainWordListId(wordListId)) { + final boolean canUse = f.canRead() && hackCanUseDictionaryFile(locale, f); + if (canUse && isMainWordListId(wordListId)) { foundMainDict = true; } if (!dictPackSettings.isWordListActive(wordListId)) continue; - if (f.canRead()) { + if (canUse) { fileList.add(AssetFileAddress.makeFromFileName(f.getPath())); } else { - Log.e(TAG, "Found a cached dictionary file but cannot read it"); + Log.e(TAG, "Found a cached dictionary file but cannot read or use it"); } } diff --git a/java/src/com/android/inputmethod/latin/CollectionUtils.java b/java/src/com/android/inputmethod/latin/CollectionUtils.java new file mode 100644 index 000000000..c75f2df5c --- /dev/null +++ b/java/src/com/android/inputmethod/latin/CollectionUtils.java @@ -0,0 +1,95 @@ +/* + * 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.latin; + +import android.util.SparseArray; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.Map; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; + +public final class CollectionUtils { + private CollectionUtils() { + // This utility class is not publicly instantiable. + } + + public static <K,V> HashMap<K,V> newHashMap() { + return new HashMap<K,V>(); + } + + public static <K,V> TreeMap<K,V> newTreeMap() { + return new TreeMap<K,V>(); + } + + public static <K, V> Map<K,V> newSynchronizedTreeMap() { + final TreeMap<K,V> treeMap = newTreeMap(); + return Collections.synchronizedMap(treeMap); + } + + public static <K,V> ConcurrentHashMap<K,V> newConcurrentHashMap() { + return new ConcurrentHashMap<K,V>(); + } + + public static <E> HashSet<E> newHashSet() { + return new HashSet<E>(); + } + + public static <E> TreeSet<E> newTreeSet() { + return new TreeSet<E>(); + } + + public static <E> ArrayList<E> newArrayList() { + return new ArrayList<E>(); + } + + public static <E> ArrayList<E> newArrayList(final int initialCapacity) { + return new ArrayList<E>(initialCapacity); + } + + public static <E> ArrayList<E> newArrayList(final Collection<E> collection) { + return new ArrayList<E>(collection); + } + + public static <E> LinkedList<E> newLinkedList() { + return new LinkedList<E>(); + } + + public static <E> CopyOnWriteArrayList<E> newCopyOnWriteArrayList() { + return new CopyOnWriteArrayList<E>(); + } + + public static <E> CopyOnWriteArrayList<E> newCopyOnWriteArrayList( + final Collection<E> collection) { + return new CopyOnWriteArrayList<E>(collection); + } + + public static <E> CopyOnWriteArrayList<E> newCopyOnWriteArrayList(final E[] array) { + return new CopyOnWriteArrayList<E>(array); + } + + public static <E> SparseArray<E> newSparseArray() { + return new SparseArray<E>(); + } +} diff --git a/java/src/com/android/inputmethod/latin/Constants.java b/java/src/com/android/inputmethod/latin/Constants.java index 1242967ad..d71c0f995 100644 --- a/java/src/com/android/inputmethod/latin/Constants.java +++ b/java/src/com/android/inputmethod/latin/Constants.java @@ -128,6 +128,13 @@ public final class Constants { } } + public static final int NOT_A_CODE = -1; + + // See {@link KeyboardActionListener.Adapter#isInvalidCoordinate(int)}. + public static final int NOT_A_COORDINATE = -1; + public static final int SUGGESTION_STRIP_COORDINATE = -2; + public static final int SPELL_CHECKER_COORDINATE = -3; + private Constants() { // This utility class is not publicly instantiable. } diff --git a/java/src/com/android/inputmethod/latin/DicTraverseSession.java b/java/src/com/android/inputmethod/latin/DicTraverseSession.java index c76815363..359da72cc 100644 --- a/java/src/com/android/inputmethod/latin/DicTraverseSession.java +++ b/java/src/com/android/inputmethod/latin/DicTraverseSession.java @@ -22,6 +22,7 @@ public class DicTraverseSession { static { JniUtils.loadNativeLibrary(); } + private native long setDicTraverseSessionNative(String locale); private native void initDicTraverseSessionNative(long nativeDicTraverseSession, long dictionary, int[] previousWord, int previousWordLength); @@ -29,9 +30,10 @@ public class DicTraverseSession { private long mNativeDicTraverseSession; - public DicTraverseSession(Locale locale) { + public DicTraverseSession(Locale locale, long dictionary) { mNativeDicTraverseSession = createNativeDicTraverseSession( locale != null ? locale.toString() : ""); + initSession(dictionary); } public long getSession() { diff --git a/java/src/com/android/inputmethod/latin/Dictionary.java b/java/src/com/android/inputmethod/latin/Dictionary.java index 60fe17b19..88d0c09dd 100644 --- a/java/src/com/android/inputmethod/latin/Dictionary.java +++ b/java/src/com/android/inputmethod/latin/Dictionary.java @@ -42,7 +42,6 @@ public abstract class Dictionary { public static final String TYPE_USER = "user"; // User history dictionary internal to LatinIME. public static final String TYPE_USER_HISTORY = "history"; - public static final String TYPE_WHITELIST ="whitelist"; protected final String mDictType; public Dictionary(final String dictType) { @@ -62,6 +61,13 @@ public abstract class Dictionary { abstract public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer, final CharSequence prevWord, final ProximityInfo proximityInfo); + // The default implementation of this method ignores sessionId. + // Subclasses that want to use sessionId need to override this method. + public ArrayList<SuggestedWordInfo> getSuggestionsWithSessionId(final WordComposer composer, + final CharSequence prevWord, final ProximityInfo proximityInfo, int sessionId) { + return getSuggestions(composer, prevWord, proximityInfo); + } + /** * Checks if the given word occurs in the dictionary * @param word the word to search for. The search should be case-insensitive. diff --git a/java/src/com/android/inputmethod/latin/DictionaryCollection.java b/java/src/com/android/inputmethod/latin/DictionaryCollection.java index ee80f2532..4acab6b05 100644 --- a/java/src/com/android/inputmethod/latin/DictionaryCollection.java +++ b/java/src/com/android/inputmethod/latin/DictionaryCollection.java @@ -35,22 +35,22 @@ public class DictionaryCollection extends Dictionary { public DictionaryCollection(final String dictType) { super(dictType); - mDictionaries = new CopyOnWriteArrayList<Dictionary>(); + mDictionaries = CollectionUtils.newCopyOnWriteArrayList(); } public DictionaryCollection(final String dictType, Dictionary... dictionaries) { super(dictType); if (null == dictionaries) { - mDictionaries = new CopyOnWriteArrayList<Dictionary>(); + mDictionaries = CollectionUtils.newCopyOnWriteArrayList(); } else { - mDictionaries = new CopyOnWriteArrayList<Dictionary>(dictionaries); + mDictionaries = CollectionUtils.newCopyOnWriteArrayList(dictionaries); mDictionaries.removeAll(Collections.singleton(null)); } } public DictionaryCollection(final String dictType, Collection<Dictionary> dictionaries) { super(dictType); - mDictionaries = new CopyOnWriteArrayList<Dictionary>(dictionaries); + mDictionaries = CollectionUtils.newCopyOnWriteArrayList(dictionaries); mDictionaries.removeAll(Collections.singleton(null)); } @@ -63,7 +63,7 @@ public class DictionaryCollection extends Dictionary { // dictionary and add the rest to it if not null, hence the get(0) ArrayList<SuggestedWordInfo> suggestions = dictionaries.get(0).getSuggestions(composer, prevWord, proximityInfo); - if (null == suggestions) suggestions = new ArrayList<SuggestedWordInfo>(); + if (null == suggestions) suggestions = CollectionUtils.newArrayList(); final int length = dictionaries.size(); for (int i = 1; i < length; ++ i) { final ArrayList<SuggestedWordInfo> sugg = dictionaries.get(i).getSuggestions(composer, diff --git a/java/src/com/android/inputmethod/latin/DictionaryFactory.java b/java/src/com/android/inputmethod/latin/DictionaryFactory.java index 06a5f4b72..cdd01d0c7 100644 --- a/java/src/com/android/inputmethod/latin/DictionaryFactory.java +++ b/java/src/com/android/inputmethod/latin/DictionaryFactory.java @@ -53,7 +53,7 @@ public class DictionaryFactory { createBinaryDictionary(context, locale)); } - final LinkedList<Dictionary> dictList = new LinkedList<Dictionary>(); + final LinkedList<Dictionary> dictList = CollectionUtils.newLinkedList(); final ArrayList<AssetFileAddress> assetFileList = BinaryDictionaryGetter.getDictionaryFiles(locale, context); if (null != assetFileList) { diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java index 016530abb..8a509be48 100644 --- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java @@ -62,7 +62,7 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { * that filename. */ private static final HashMap<String, DictionaryController> sSharedDictionaryControllers = - new HashMap<String, DictionaryController>(); + CollectionUtils.newHashMap(); /** The application context. */ protected final Context mContext; @@ -159,9 +159,9 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { * the native side. */ public void clearFusionDictionary() { + final HashMap<String, String> attributes = CollectionUtils.newHashMap(); mFusionDictionary = new FusionDictionary(new Node(), - new FusionDictionary.DictionaryOptions(new HashMap<String, String>(), false, - false)); + new FusionDictionary.DictionaryOptions(attributes, false, false)); } /** @@ -172,12 +172,12 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { // considering performance regression. protected void addWord(final String word, final String shortcutTarget, final int frequency) { if (shortcutTarget == null) { - mFusionDictionary.add(word, frequency, null); + mFusionDictionary.add(word, frequency, null, false /* isNotAWord */); } else { // TODO: Do this in the subclass, with this class taking an arraylist. - final ArrayList<WeightedString> shortcutTargets = new ArrayList<WeightedString>(); + final ArrayList<WeightedString> shortcutTargets = CollectionUtils.newArrayList(); shortcutTargets.add(new WeightedString(shortcutTarget, frequency)); - mFusionDictionary.add(word, frequency, shortcutTargets); + mFusionDictionary.add(word, frequency, shortcutTargets, false /* isNotAWord */); } } diff --git a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java index d101aaf15..8a38d1e1b 100644 --- a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java +++ b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java @@ -19,7 +19,6 @@ package com.android.inputmethod.latin; import android.content.Context; import android.text.TextUtils; -import com.android.inputmethod.keyboard.KeyDetector; import com.android.inputmethod.keyboard.Keyboard; import com.android.inputmethod.keyboard.ProximityInfo; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; @@ -231,7 +230,7 @@ public class ExpandableDictionary extends Dictionary { childNode.mTerminal = true; if (isShortcutOnly) { if (null == childNode.mShortcutTargets) { - childNode.mShortcutTargets = new ArrayList<char[]>(); + childNode.mShortcutTargets = CollectionUtils.newArrayList(); } childNode.mShortcutTargets.add(shortcutTarget.toCharArray()); } else { @@ -251,7 +250,7 @@ public class ExpandableDictionary extends Dictionary { public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer, final CharSequence prevWord, final ProximityInfo proximityInfo) { if (reloadDictionaryIfRequired()) return null; - if (composer.size() <= 1) { + if (composer.size() > 1) { if (composer.size() >= BinaryDictionary.MAX_WORD_LENGTH) { return null; } @@ -260,7 +259,7 @@ public class ExpandableDictionary extends Dictionary { return suggestions; } else { if (TextUtils.isEmpty(prevWord)) return null; - final ArrayList<SuggestedWordInfo> suggestions = new ArrayList<SuggestedWordInfo>(); + final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList(); runBigramReverseLookUp(prevWord, suggestions); return suggestions; } @@ -279,7 +278,7 @@ public class ExpandableDictionary extends Dictionary { protected ArrayList<SuggestedWordInfo> getWordsInner(final WordComposer codes, final CharSequence prevWordForBigrams, final ProximityInfo proximityInfo) { - final ArrayList<SuggestedWordInfo> suggestions = new ArrayList<SuggestedWordInfo>(); + final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList(); mInputLength = codes.size(); if (mCodes.length < mInputLength) mCodes = new int[mInputLength][]; final InputPointers ips = codes.getInputPointers(); @@ -292,9 +291,9 @@ public class ExpandableDictionary extends Dictionary { mCodes[i] = new int[ProximityInfo.MAX_PROXIMITY_CHARS_SIZE]; } final int x = xCoordinates != null && i < xCoordinates.length ? - xCoordinates[i] : WordComposer.NOT_A_COORDINATE; + xCoordinates[i] : Constants.NOT_A_COORDINATE; final int y = xCoordinates != null && i < yCoordinates.length ? - yCoordinates[i] : WordComposer.NOT_A_COORDINATE; + yCoordinates[i] : Constants.NOT_A_COORDINATE; proximityInfo.fillArrayWithNearestKeyCodes(x, y, codes.getCodeAt(i), mCodes[i]); } mMaxDepth = mInputLength * 3; @@ -487,7 +486,7 @@ public class ExpandableDictionary extends Dictionary { for (int j = 0; j < alternativesSize; j++) { final int addedAttenuation = (j > 0 ? 1 : 2); final int currentChar = currentChars[j]; - if (currentChar == KeyDetector.NOT_A_CODE) { + if (currentChar == Constants.NOT_A_CODE) { break; } if (currentChar == lowerC || currentChar == c) { @@ -551,7 +550,7 @@ public class ExpandableDictionary extends Dictionary { Node secondWord = searchWord(mRoots, word2, 0, null); LinkedList<NextWord> bigrams = firstWord.mNGrams; if (bigrams == null || bigrams.size() == 0) { - firstWord.mNGrams = new LinkedList<NextWord>(); + firstWord.mNGrams = CollectionUtils.newLinkedList(); bigrams = firstWord.mNGrams; } else { for (NextWord nw : bigrams) { diff --git a/java/src/com/android/inputmethod/latin/ImfUtils.java b/java/src/com/android/inputmethod/latin/ImfUtils.java index 1461c0240..2674e4575 100644 --- a/java/src/com/android/inputmethod/latin/ImfUtils.java +++ b/java/src/com/android/inputmethod/latin/ImfUtils.java @@ -29,7 +29,7 @@ import java.util.List; /** * Utility class for Input Method Framework */ -public class ImfUtils { +public final class ImfUtils { private ImfUtils() { // This utility class is not publicly instantiable. } diff --git a/java/src/com/android/inputmethod/latin/InputAttributes.java b/java/src/com/android/inputmethod/latin/InputAttributes.java index e561f5956..7bcda9bc4 100644 --- a/java/src/com/android/inputmethod/latin/InputAttributes.java +++ b/java/src/com/android/inputmethod/latin/InputAttributes.java @@ -29,10 +29,12 @@ public class InputAttributes { final public boolean mInputTypeNoAutoCorrect; final public boolean mIsSettingsSuggestionStripOn; final public boolean mApplicationSpecifiedCompletionOn; + final private int mInputType; public InputAttributes(final EditorInfo editorInfo, final boolean isFullscreenMode) { final int inputType = null != editorInfo ? editorInfo.inputType : 0; final int inputClass = inputType & InputType.TYPE_MASK_CLASS; + mInputType = inputType; if (inputClass != InputType.TYPE_CLASS_TEXT) { // If we are not looking at a TYPE_CLASS_TEXT field, the following strange // cases may arise, so we do a couple sanity checks for them. If it's a @@ -93,6 +95,10 @@ public class InputAttributes { } } + public boolean isSameInputType(final EditorInfo editorInfo) { + return editorInfo.inputType == mInputType; + } + @SuppressWarnings("unused") private void dumpFlags(final int inputType) { Log.i(TAG, "Input class:"); diff --git a/java/src/com/android/inputmethod/latin/InputPointers.java b/java/src/com/android/inputmethod/latin/InputPointers.java index cbc916a7e..ff2feb51d 100644 --- a/java/src/com/android/inputmethod/latin/InputPointers.java +++ b/java/src/com/android/inputmethod/latin/InputPointers.java @@ -93,7 +93,7 @@ public class InputPointers { } mXCoordinates.append(xCoordinates, startPos, length); mYCoordinates.append(yCoordinates, startPos, length); - mPointerIds.fill(pointerId, startPos, length); + mPointerIds.fill(pointerId, mPointerIds.getLength(), length); mTimes.append(times, startPos, length); } @@ -124,4 +124,10 @@ public class InputPointers { public int[] getTimes() { return mTimes.getPrimitiveArray(); } + + @Override + public String toString() { + return "size=" + getPointerSize() + " id=" + mPointerIds + " time=" + mTimes + + " x=" + mXCoordinates + " y=" + mYCoordinates; + } } diff --git a/java/src/com/android/inputmethod/latin/InputTypeUtils.java b/java/src/com/android/inputmethod/latin/InputTypeUtils.java index 40c3b765e..500866a13 100644 --- a/java/src/com/android/inputmethod/latin/InputTypeUtils.java +++ b/java/src/com/android/inputmethod/latin/InputTypeUtils.java @@ -18,7 +18,7 @@ package com.android.inputmethod.latin; import android.text.InputType; -public class InputTypeUtils implements InputType { +public final class InputTypeUtils implements InputType { private static final int WEB_TEXT_PASSWORD_INPUT_TYPE = TYPE_CLASS_TEXT | TYPE_TEXT_VARIATION_WEB_PASSWORD; private static final int WEB_TEXT_EMAIL_ADDRESS_INPUT_TYPE = diff --git a/java/src/com/android/inputmethod/latin/JniUtils.java b/java/src/com/android/inputmethod/latin/JniUtils.java index 86a3826d8..f9305991a 100644 --- a/java/src/com/android/inputmethod/latin/JniUtils.java +++ b/java/src/com/android/inputmethod/latin/JniUtils.java @@ -20,7 +20,7 @@ import android.util.Log; import com.android.inputmethod.latin.define.JniLibName; -public class JniUtils { +public final class JniUtils { private static final String TAG = JniUtils.class.getSimpleName(); private JniUtils() { diff --git a/java/src/com/android/inputmethod/latin/LastComposedWord.java b/java/src/com/android/inputmethod/latin/LastComposedWord.java index bb39ce4f7..dd73a978c 100644 --- a/java/src/com/android/inputmethod/latin/LastComposedWord.java +++ b/java/src/com/android/inputmethod/latin/LastComposedWord.java @@ -38,12 +38,12 @@ public class LastComposedWord { // an auto-correction. public static final int COMMIT_TYPE_CANCEL_AUTO_CORRECT = 3; - public static final int NOT_A_SEPARATOR = -1; + public static final String NOT_A_SEPARATOR = ""; public final int[] mPrimaryKeyCodes; public final String mTypedWord; public final String mCommittedWord; - public final int mSeparatorCode; + public final String mSeparatorString; public final CharSequence mPrevWord; public final InputPointers mInputPointers = new InputPointers(BinaryDictionary.MAX_WORD_LENGTH); @@ -56,14 +56,14 @@ public class LastComposedWord { // immutable. Do not fiddle with their contents after you passed them to this constructor. public LastComposedWord(final int[] primaryKeyCodes, final InputPointers inputPointers, final String typedWord, final String committedWord, - final int separatorCode, final CharSequence prevWord) { + final String separatorString, final CharSequence prevWord) { mPrimaryKeyCodes = primaryKeyCodes; if (inputPointers != null) { mInputPointers.copy(inputPointers); } mTypedWord = typedWord; mCommittedWord = committedWord; - mSeparatorCode = separatorCode; + mSeparatorString = separatorString; mActive = true; mPrevWord = prevWord; } @@ -80,7 +80,7 @@ public class LastComposedWord { return TextUtils.equals(mTypedWord, mCommittedWord); } - public static int getSeparatorLength(final int separatorCode) { - return NOT_A_SEPARATOR == separatorCode ? 0 : 1; + public static int getSeparatorLength(final String separatorString) { + return StringUtils.codePointCount(separatorString); } } diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index 446d44e7a..39c3a808f 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -361,7 +361,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mPrefs = prefs; LatinImeLogger.init(this, prefs); if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.getInstance().init(this, prefs, mKeyboardSwitcher); + ResearchLogger.getInstance().init(this, prefs); } InputMethodManagerCompatWrapper.init(this); SubtypeSwitcher.init(this); @@ -381,18 +381,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen ImfUtils.setAdditionalInputMethodSubtypes(this, mCurrentSettings.getAdditionalSubtypes()); - Utils.GCUtils.getInstance().reset(); - boolean tryGC = true; - // Shouldn't this be removed? I think that from Honeycomb on, the GC is now actually working - // as expected and this code is useless. - for (int i = 0; i < Utils.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) { - try { - initSuggest(); - tryGC = false; - } catch (OutOfMemoryError e) { - tryGC = Utils.GCUtils.getInstance().tryGCOrWait("InitSuggest", e); - } - } + initSuggest(); mDisplayOrientation = res.getConfiguration().orientation; @@ -416,7 +405,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } // Has to be package-visible for unit tests - /* package */ void loadSettings() { + /* package for test */ + void loadSettings() { // Note that the calling sequence of onCreate() and onCurrentInputMethodSubtypeChanged() // is not guaranteed. It may even be called at the same time on a different thread. if (null == mPrefs) mPrefs = PreferenceManager.getDefaultSharedPreferences(this); @@ -540,7 +530,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen @Override public void onConfigurationChanged(Configuration conf) { - mSubtypeSwitcher.onConfigurationChanged(conf); + // System locale has been changed. Needs to reload keyboard. + if (mSubtypeSwitcher.onConfigurationChanged(conf, this)) { + loadKeyboard(); + } // If orientation changed while predicting, commit the change if (mDisplayOrientation != conf.orientation) { mDisplayOrientation = conf.orientation; @@ -607,6 +600,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // Note that the calling sequence of onCreate() and onCurrentInputMethodSubtypeChanged() // is not guaranteed. It may even be called at the same time on a different thread. mSubtypeSwitcher.updateSubtype(subtype); + loadKeyboard(); } private void onStartInputInternal(EditorInfo editorInfo, boolean restarting) { @@ -670,8 +664,20 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen accessUtils.onStartInputViewInternal(mainKeyboardView, editorInfo, restarting); } - if (!restarting) { - mSubtypeSwitcher.updateParametersOnStartInputView(); + final boolean selectionChanged = mLastSelectionStart != editorInfo.initialSelStart + || mLastSelectionEnd != editorInfo.initialSelEnd; + final boolean inputTypeChanged = !mCurrentSettings.isSameInputType(editorInfo); + final boolean isDifferentTextField = !restarting || inputTypeChanged; + if (isDifferentTextField) { + final boolean currentSubtypeEnabled = mSubtypeSwitcher + .updateParametersOnStartInputViewAndReturnIfCurrentSubtypeEnabled(); + if (!currentSubtypeEnabled) { + // Current subtype is disabled. Needs to update subtype and keyboard. + final InputMethodSubtype newSubtype = ImfUtils.getCurrentInputMethodSubtype( + this, mSubtypeSwitcher.getNoLanguageSubtype()); + mSubtypeSwitcher.updateSubtype(newSubtype); + loadKeyboard(); + } } // The EditorInfo might have a flag that affects fullscreen mode. @@ -679,9 +685,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen updateFullscreenMode(); mApplicationSpecifiedCompletions = null; - final boolean selectionChanged = mLastSelectionStart != editorInfo.initialSelStart - || mLastSelectionEnd != editorInfo.initialSelEnd; - if (!restarting || selectionChanged) { + if (isDifferentTextField || selectionChanged) { // If the selection changed, we reset the input state. Essentially, we come here with // restarting == true when the app called setText() or similar. We should reset the // state if the app set the text to something else, but keep it if it set a suggestion @@ -696,7 +700,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } } - if (!restarting) { + if (isDifferentTextField) { mainKeyboardView.closing(); loadSettings(); @@ -905,13 +909,13 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } } } - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_onDisplayCompletions(applicationSpecifiedCompletions); - } if (!mCurrentSettings.isApplicationSpecifiedCompletionsOn()) return; mApplicationSpecifiedCompletions = applicationSpecifiedCompletions; if (applicationSpecifiedCompletions == null) { clearSuggestionStrip(); + if (ProductionFlag.IS_EXPERIMENTAL) { + ResearchLogger.latinIME_onDisplayCompletions(null); + } return; } @@ -933,6 +937,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // this case? This says to keep whatever the user typed. mWordComposer.setAutoCorrection(mWordComposer.getTypedWord()); setSuggestionStripShown(true); + if (ProductionFlag.IS_EXPERIMENTAL) { + ResearchLogger.latinIME_onDisplayCompletions(applicationSpecifiedCompletions); + } } private void setSuggestionStripShownInternal(boolean shown, boolean needsInputViewShown) { @@ -1048,18 +1055,15 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; } - private void commitTyped(final int separatorCode) { + private void commitTyped(final String separatorString) { if (!mWordComposer.isComposingWord()) return; final CharSequence typedWord = mWordComposer.getTypedWord(); if (typedWord.length() > 0) { mConnection.commitText(typedWord, 1); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_commitText(typedWord); - } final CharSequence prevWord = addToUserHistoryDictionary(typedWord); mLastComposedWord = mWordComposer.commitWord( LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD, typedWord.toString(), - separatorCode, prevWord); + separatorString, prevWord); } } @@ -1091,18 +1095,27 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen return mConnection.getCursorCapsMode(inputType); } + // Factor in auto-caps and manual caps and compute the current caps mode. + private int getActualCapsMode() { + final int manual = mKeyboardSwitcher.getManualCapsMode(); + if (manual != WordComposer.CAPS_MODE_OFF) return manual; + final int auto = getCurrentAutoCapsState(); + if (0 != (auto & TextUtils.CAP_MODE_CHARACTERS)) { + return WordComposer.CAPS_MODE_AUTO_SHIFT_LOCKED; + } + if (0 != auto) return WordComposer.CAPS_MODE_AUTO_SHIFTED; + return WordComposer.CAPS_MODE_OFF; + } + private void swapSwapperAndSpace() { CharSequence lastTwo = mConnection.getTextBeforeCursor(2, 0); // It is guaranteed lastTwo.charAt(1) is a swapper - else this method is not called. if (lastTwo != null && lastTwo.length() == 2 && lastTwo.charAt(0) == Keyboard.CODE_SPACE) { mConnection.deleteSurroundingText(2, 0); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_deleteSurroundingText(2); - } mConnection.commitText(lastTwo.charAt(1) + " ", 1); if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_swapSwapperAndSpaceWhileInBatchEdit(); + ResearchLogger.latinIME_swapSwapperAndSpace(); } mKeyboardSwitcher.updateShiftState(); } @@ -1119,9 +1132,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mHandler.cancelDoubleSpacesTimer(); mConnection.deleteSurroundingText(2, 0); mConnection.commitText(". ", 1); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_doubleSpaceAutoPeriod(); - } mKeyboardSwitcher.updateShiftState(); return true; } @@ -1185,9 +1195,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private void performEditorAction(int actionId) { mConnection.performEditorAction(actionId); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_performEditorAction(actionId); - } } private void handleLanguageSwitchKey() { @@ -1224,6 +1231,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}. if (code >= '0' && code <= '9') { super.sendKeyChar((char)code); + if (ProductionFlag.IS_EXPERIMENTAL) { + ResearchLogger.latinIME_sendKeyCodePoint(code); + } return; } @@ -1240,9 +1250,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen final String text = new String(new int[] { code }, 0, 1); mConnection.commitText(text, text.length()); } - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_sendKeyCodePoint(code); - } } // Implementation of {@link KeyboardActionListener}. @@ -1254,11 +1261,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } mLastKeyTime = when; mConnection.beginBatchEdit(); - - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_onCodeInput(primaryCode, x, y); - } - final KeyboardSwitcher switcher = mKeyboardSwitcher; // The space state depends only on the last character pressed and its own previous // state. Here, we revert the space state to neutral if the key is actually modifying @@ -1291,7 +1293,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen onSettingsKeyPressed(); break; case Keyboard.CODE_SHORTCUT: - mSubtypeSwitcher.switchToShortcutIME(); + mSubtypeSwitcher.switchToShortcutIME(this); break; case Keyboard.CODE_ACTION_ENTER: performEditorAction(getActionId(switcher.getKeyboard())); @@ -1307,7 +1309,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen break; case Keyboard.CODE_RESEARCH: if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.getInstance().presentResearchDialog(this); + ResearchLogger.getInstance().onResearchKeySelected(this); } break; default: @@ -1324,8 +1326,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen keyX = x; keyY = y; } else { - keyX = NOT_A_TOUCH_COORDINATE; - keyY = NOT_A_TOUCH_COORDINATE; + keyX = Constants.NOT_A_COORDINATE; + keyY = Constants.NOT_A_COORDINATE; } handleCharacter(primaryCode, keyX, keyY, spaceState); } @@ -1338,45 +1340,49 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen if (!didAutoCorrect && primaryCode != Keyboard.CODE_SHIFT && primaryCode != Keyboard.CODE_SWITCH_ALPHA_SYMBOL) mLastComposedWord.deactivate(); - mEnteredText = null; + if (Keyboard.CODE_DELETE != primaryCode) { + mEnteredText = null; + } mConnection.endBatchEdit(); + if (ProductionFlag.IS_EXPERIMENTAL) { + ResearchLogger.latinIME_onCodeInput(primaryCode, x, y); + } } // Called from PointerTracker through the KeyboardActionListener interface @Override public void onTextInput(CharSequence rawText) { mConnection.beginBatchEdit(); - commitTyped(LastComposedWord.NOT_A_SEPARATOR); + if (mWordComposer.isComposingWord()) { + commitCurrentAutoCorrection(rawText.toString()); + } else { + resetComposingState(true /* alsoResetLastComposedWord */); + } mHandler.postUpdateSuggestionStrip(); final CharSequence text = specificTldProcessingOnTextInput(rawText); if (SPACE_STATE_PHANTOM == mSpaceState) { sendKeyCodePoint(Keyboard.CODE_SPACE); } mConnection.commitText(text, 1); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_commitText(text); - } mConnection.endBatchEdit(); mKeyboardSwitcher.updateShiftState(); mKeyboardSwitcher.onCodeInput(Keyboard.CODE_OUTPUT_TEXT); mSpaceState = SPACE_STATE_NONE; mEnteredText = text; - resetComposingState(true /* alsoResetLastComposedWord */); } @Override public void onStartBatchInput() { mConnection.beginBatchEdit(); if (mWordComposer.isComposingWord()) { - commitTyped(LastComposedWord.NOT_A_SEPARATOR); + commitCurrentAutoCorrection(LastComposedWord.NOT_A_SEPARATOR); mExpectingUpdateSelection = true; // TODO: Can we remove this? mSpaceState = SPACE_STATE_PHANTOM; } mConnection.endBatchEdit(); // TODO: Should handle TextUtils.CAP_MODE_CHARACTER. - mWordComposer.setAutoCapitalized( - getCurrentAutoCapsState() != Constants.TextUtils.CAP_MODE_OFF); + mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode()); } @Override @@ -1448,21 +1454,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // In many cases, we may have to put the keyboard in auto-shift state again. mHandler.postUpdateShiftState(); - if (mEnteredText != null && mConnection.sameAsTextBeforeCursor(mEnteredText)) { - // Cancel multi-character input: remove the text we just entered. - // This is triggered on backspace after a key that inputs multiple characters, - // like the smiley key or the .com key. - final int length = mEnteredText.length(); - mConnection.deleteSurroundingText(length, 0); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_deleteSurroundingText(length); - } - // If we have mEnteredText, then we know that mHasUncommittedTypedChars == false. - // In addition we know that spaceState is false, and that we should not be - // reverting any autocorrect at this point. So we can safely return. - return; - } - if (mWordComposer.isComposingWord()) { final int length = mWordComposer.size(); if (length > 0) { @@ -1476,9 +1467,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mHandler.postUpdateSuggestionStrip(); } else { mConnection.deleteSurroundingText(1, 0); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_deleteSurroundingText(1); - } } } else { if (mLastComposedWord.canRevertCommit()) { @@ -1486,6 +1474,18 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen revertCommit(); return; } + if (mEnteredText != null && mConnection.sameAsTextBeforeCursor(mEnteredText)) { + // Cancel multi-character input: remove the text we just entered. + // This is triggered on backspace after a key that inputs multiple characters, + // like the smiley key or the .com key. + final int length = mEnteredText.length(); + mConnection.deleteSurroundingText(length, 0); + mEnteredText = null; + // If we have mEnteredText, then we know that mHasUncommittedTypedChars == false. + // In addition we know that spaceState is false, and that we should not be + // reverting any autocorrect at this point. So we can safely return. + return; + } if (SPACE_STATE_DOUBLE == spaceState) { mHandler.cancelDoubleSpacesTimer(); if (mConnection.revertDoubleSpace()) { @@ -1507,9 +1507,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen final int lengthToDelete = mLastSelectionEnd - mLastSelectionStart; mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd); mConnection.deleteSurroundingText(lengthToDelete, 0); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_deleteSurroundingText(lengthToDelete); - } } else { // There is no selection, just delete one character. if (NOT_A_CURSOR_POSITION == mLastSelectionEnd) { @@ -1528,14 +1525,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } else { mConnection.deleteSurroundingText(1, 0); } - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_deleteSurroundingText(1); - } if (mDeleteCount > DELETE_ACCELERATE_AT) { mConnection.deleteSurroundingText(1, 0); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_deleteSurroundingText(1); - } } } if (mCurrentSettings.isSuggestionsRequested(mDisplayOrientation)) { @@ -1611,13 +1602,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mWordComposer.add(primaryCode, keyX, keyY); // If it's the first letter, make note of auto-caps state if (mWordComposer.size() == 1) { - mWordComposer.setAutoCapitalized( - getCurrentAutoCapsState() != Constants.TextUtils.CAP_MODE_OFF); + mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode()); } mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1); } else { final boolean swapWeakSpace = maybeStripSpace(primaryCode, - spaceState, KeyboardActionListener.SUGGESTION_STRIP_COORDINATE == x); + spaceState, Constants.SUGGESTION_STRIP_COORDINATE == x); sendKeyCodePoint(primaryCode); @@ -1639,15 +1629,16 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // Handle separator if (mWordComposer.isComposingWord()) { if (mCurrentSettings.mCorrectionEnabled) { - commitCurrentAutoCorrection(primaryCode); + // TODO: maybe cache Strings in an <String> sparse array or something + commitCurrentAutoCorrection(new String(new int[]{primaryCode}, 0, 1)); didAutoCorrect = true; } else { - commitTyped(primaryCode); + commitTyped(new String(new int[]{primaryCode}, 0, 1)); } } final boolean swapWeakSpace = maybeStripSpace(primaryCode, spaceState, - KeyboardActionListener.SUGGESTION_STRIP_COORDINATE == x); + Constants.SUGGESTION_STRIP_COORDINATE == x); if (SPACE_STATE_PHANTOM == spaceState && mCurrentSettings.isPhantomSpacePromotingSymbol(primaryCode)) { @@ -1694,6 +1685,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen Utils.Stats.onSeparator((char)primaryCode, x, y); + mHandler.postUpdateShiftState(); return didAutoCorrect; } @@ -1714,7 +1706,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // TODO: make this private // Outside LatinIME, only used by the test suite. - /* package for tests */ boolean isShowingPunctuationList() { + /* package for tests */ + boolean isShowingPunctuationList() { if (mSuggestionStripView == null) return false; return mCurrentSettings.mSuggestPuncList == mSuggestionStripView.getSuggestions(); } @@ -1845,7 +1838,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen setSuggestionStripShown(isSuggestionsStripVisible()); } - private void commitCurrentAutoCorrection(final int separatorCodePoint) { + private void commitCurrentAutoCorrection(final String separatorString) { // Complete any pending suggestions query first if (mHandler.hasPendingUpdateSuggestions()) { updateSuggestionStrip(); @@ -1859,14 +1852,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen throw new RuntimeException("We have an auto-correction but the typed word " + "is empty? Impossible! I must commit suicide."); } - Utils.Stats.onAutoCorrection(typedWord, autoCorrection.toString(), separatorCodePoint); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_commitCurrentAutoCorrection(typedWord, - autoCorrection.toString()); - } + Utils.Stats.onAutoCorrection(typedWord, autoCorrection.toString(), separatorString); mExpectingUpdateSelection = true; commitChosenWord(autoCorrection, LastComposedWord.COMMIT_TYPE_DECIDED_WORD, - separatorCodePoint); + separatorString); if (!typedWord.equals(autoCorrection)) { // This will make the correction flash for a short while as a visual clue // to the user that auto-correction happened. @@ -1880,8 +1869,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // Called from {@link SuggestionStripView} through the {@link SuggestionStripView#Listener} // interface @Override - public void pickSuggestionManually(final int index, final CharSequence suggestion, - final int x, final int y) { + public void pickSuggestionManually(final int index, final CharSequence suggestion) { final SuggestedWords suggestedWords = mSuggestionStripView.getSuggestions(); // If this is a punctuation picked from the suggestion strip, pass it to onCodeInput if (suggestion.length() == 1 && isShowingPunctuationList()) { @@ -1889,13 +1877,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // So, LatinImeLogger logs "" as a user's input. LatinImeLogger.logOnManualSuggestion("", suggestion.toString(), index, suggestedWords); // Rely on onCodeInput to do the complicated swapping/stripping logic consistently. - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_punctuationSuggestion(index, suggestion, x, y); - } final int primaryCode = suggestion.charAt(0); onCodeInput(primaryCode, - KeyboardActionListener.SUGGESTION_STRIP_COORDINATE, - KeyboardActionListener.SUGGESTION_STRIP_COORDINATE); + Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE); + if (ProductionFlag.IS_EXPERIMENTAL) { + ResearchLogger.latinIME_punctuationSuggestion(index, suggestion); + } return; } @@ -1922,10 +1909,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen final CompletionInfo completionInfo = mApplicationSpecifiedCompletions[index]; mConnection.commitCompletion(completionInfo); mConnection.endBatchEdit(); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_pickApplicationSpecifiedCompletion(index, - completionInfo.getText(), x, y); - } return; } @@ -1934,12 +1917,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen final String replacedWord = mWordComposer.getTypedWord().toString(); LatinImeLogger.logOnManualSuggestion(replacedWord, suggestion.toString(), index, suggestedWords); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_pickSuggestionManually(replacedWord, index, suggestion, x, y); - } mExpectingUpdateSelection = true; commitChosenWord(suggestion, LastComposedWord.COMMIT_TYPE_MANUAL_PICK, LastComposedWord.NOT_A_SEPARATOR); + if (ProductionFlag.IS_EXPERIMENTAL) { + ResearchLogger.latinIME_pickSuggestionManually(replacedWord, index, suggestion); + } mConnection.endBatchEdit(); // Don't allow cancellation of manual pick mLastComposedWord.deactivate(); @@ -1955,8 +1938,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // If the suggestion is not in the dictionary, the hint should be shown. && !AutoCorrection.isValidWord(mSuggest.getUnigramDictionaries(), suggestion, true); - Utils.Stats.onSeparator((char)Keyboard.CODE_SPACE, WordComposer.NOT_A_COORDINATE, - WordComposer.NOT_A_COORDINATE); + Utils.Stats.onSeparator((char)Keyboard.CODE_SPACE, + Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); if (showingAddToDictionaryHint && mIsUserDictionaryAvailable) { mSuggestionStripView.showAddToDictionaryHint( suggestion, mCurrentSettings.mHintToSaveText); @@ -1970,13 +1953,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen * Commits the chosen word to the text field and saves it for later retrieval. */ private void commitChosenWord(final CharSequence chosenWord, final int commitType, - final int separatorCode) { + final String separatorString) { final SuggestedWords suggestedWords = mSuggestionStripView.getSuggestions(); mConnection.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan( this, chosenWord, suggestedWords, mIsMainDictionaryAvailable), 1); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_commitText(chosenWord); - } // Add the word to the user history dictionary final CharSequence prevWord = addToUserHistoryDictionary(chosenWord); // TODO: figure out here if this is an auto-correct or if the best word is actually @@ -1984,7 +1964,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // LastComposedWord#didCommitTypedWord by string equality of the remembered // strings. mLastComposedWord = mWordComposer.commitWord(commitType, chosenWord.toString(), - separatorCode, prevWord); + separatorString, prevWord); } private void setPunctuationSuggestions() { @@ -1999,6 +1979,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private CharSequence addToUserHistoryDictionary(final CharSequence suggestion) { if (TextUtils.isEmpty(suggestion)) return null; + if (mSuggest == null) return null; // If correction is not enabled, we don't add words to the user history dictionary. // That's to avoid unintended additions in some sensitive fields, or fields that @@ -2010,7 +1991,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen final CharSequence prevWord = mConnection.getNthPreviousWord(mCurrentSettings.mWordSeparators, 2); final String secondWord; - if (mWordComposer.isAutoCapitalized() && !mWordComposer.isMostlyCaps()) { + if (mWordComposer.wasAutoCapitalized() && !mWordComposer.isMostlyCaps()) { secondWord = suggestion.toString().toLowerCase( mSubtypeSwitcher.getCurrentSubtypeLocale()); } else { @@ -2043,9 +2024,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mWordComposer.setComposingWord(word, mKeyboardSwitcher.getKeyboard()); final int length = word.length(); mConnection.deleteSurroundingText(length, 0); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_deleteSurroundingText(length); - } mConnection.setComposingText(word, 1); mHandler.postUpdateSuggestionStrip(); } @@ -2056,7 +2034,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen final CharSequence committedWord = mLastComposedWord.mCommittedWord; final int cancelLength = committedWord.length(); final int separatorLength = LastComposedWord.getSeparatorLength( - mLastComposedWord.mSeparatorCode); + mLastComposedWord.mSeparatorString); // TODO: should we check our saved separator against the actual contents of the text view? final int deleteLength = cancelLength + separatorLength; if (DEBUG) { @@ -2073,18 +2051,13 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } } mConnection.deleteSurroundingText(deleteLength, 0); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_deleteSurroundingText(deleteLength); - } if (!TextUtils.isEmpty(previousWord) && !TextUtils.isEmpty(committedWord)) { mUserHistoryDictionary.cancelAddingUserHistory( previousWord.toString(), committedWord.toString()); } - mConnection.commitText(originallyTypedWord, 1); - // Re-insert the separator - sendKeyCodePoint(mLastComposedWord.mSeparatorCode); - Utils.Stats.onSeparator(mLastComposedWord.mSeparatorCode, WordComposer.NOT_A_COORDINATE, - WordComposer.NOT_A_COORDINATE); + mConnection.commitText(originallyTypedWord + mLastComposedWord.mSeparatorString, 1); + Utils.Stats.onSeparator(mLastComposedWord.mSeparatorString, + Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); if (ProductionFlag.IS_EXPERIMENTAL) { ResearchLogger.latinIME_revertCommit(originallyTypedWord); } @@ -2100,9 +2073,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen return mCurrentSettings.isWordSeparator(code); } - // Notify that language or mode have been changed and toggleLanguage will update KeyboardID - // according to new language or mode. Called from SubtypeSwitcher. - public void onRefreshKeyboard() { + // TODO: Make this private + // Outside LatinIME, only used by the {@link InputTestsBase} test suite. + /* package for test */ + void loadKeyboard() { // When the device locale is changed in SetupWizard etc., this method may get called via // onConfigurationChanged before SoftInputWindow is shown. initSuggest(); diff --git a/java/src/com/android/inputmethod/latin/LocaleUtils.java b/java/src/com/android/inputmethod/latin/LocaleUtils.java index b938dd336..feb1b2d0e 100644 --- a/java/src/com/android/inputmethod/latin/LocaleUtils.java +++ b/java/src/com/android/inputmethod/latin/LocaleUtils.java @@ -31,7 +31,10 @@ import java.util.Locale; * update/bugfix to this file, consider also updating/fixing the version in the * dictionary pack. */ -public class LocaleUtils { +public final class LocaleUtils { + private static final HashMap<String, Long> EMPTY_LT_HASH_MAP = CollectionUtils.newHashMap(); + private static final String LOCALE_AND_TIME_STR_SEPARATER = ","; + private LocaleUtils() { // Intentional empty constructor for utility class. } @@ -193,7 +196,7 @@ public class LocaleUtils { } } - private static final HashMap<String, Locale> sLocaleCache = new HashMap<String, Locale>(); + private static final HashMap<String, Locale> sLocaleCache = CollectionUtils.newHashMap(); /** * Creates a locale from a string specification. @@ -219,4 +222,38 @@ public class LocaleUtils { return retval; } } + + public static HashMap<String, Long> localeAndTimeStrToHashMap(String str) { + if (TextUtils.isEmpty(str)) { + return EMPTY_LT_HASH_MAP; + } + final String[] ss = str.split(LOCALE_AND_TIME_STR_SEPARATER); + final int N = ss.length; + if (N < 2 || N % 2 != 0) { + return EMPTY_LT_HASH_MAP; + } + final HashMap<String, Long> retval = CollectionUtils.newHashMap(); + for (int i = 0; i < N / 2; ++i) { + final String localeStr = ss[i * 2]; + final long time = Long.valueOf(ss[i * 2 + 1]); + retval.put(localeStr, time); + } + return retval; + } + + public static String localeAndTimeHashMapToStr(HashMap<String, Long> map) { + if (map == null || map.isEmpty()) { + return ""; + } + final StringBuilder builder = new StringBuilder(); + for (String localeStr : map.keySet()) { + if (builder.length() > 0) { + builder.append(LOCALE_AND_TIME_STR_SEPARATER); + } + final Long time = map.get(localeStr); + builder.append(localeStr).append(LOCALE_AND_TIME_STR_SEPARATER); + builder.append(String.valueOf(time)); + } + return builder.toString(); + } } diff --git a/java/src/com/android/inputmethod/latin/ResizableIntArray.java b/java/src/com/android/inputmethod/latin/ResizableIntArray.java index 387d45a53..c660f92c4 100644 --- a/java/src/com/android/inputmethod/latin/ResizableIntArray.java +++ b/java/src/com/android/inputmethod/latin/ResizableIntArray.java @@ -131,4 +131,16 @@ public class ResizableIntArray { mLength = endPos; } } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < mLength; i++) { + if (i != 0) { + sb.append(","); + } + sb.append(mArray[i]); + } + return "[" + sb + "]"; + } } diff --git a/java/src/com/android/inputmethod/latin/ResourceUtils.java b/java/src/com/android/inputmethod/latin/ResourceUtils.java new file mode 100644 index 000000000..5021ad384 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/ResourceUtils.java @@ -0,0 +1,128 @@ +/* + * 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.latin; + +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.os.Build; +import android.util.TypedValue; + +import java.util.HashMap; + +public final class ResourceUtils { + public static final float UNDEFINED_RATIO = -1.0f; + public static final int UNDEFINED_DIMENSION = -1; + + private ResourceUtils() { + // This utility class is not publicly instantiable. + } + + private static final String HARDWARE_PREFIX = Build.HARDWARE + ","; + private static final HashMap<String, String> sDeviceOverrideValueMap = + CollectionUtils.newHashMap(); + + public static String getDeviceOverrideValue(Resources res, int overrideResId, String defValue) { + final int orientation = res.getConfiguration().orientation; + final String key = overrideResId + "-" + orientation; + if (!sDeviceOverrideValueMap.containsKey(key)) { + String overrideValue = defValue; + for (final String element : res.getStringArray(overrideResId)) { + if (element.startsWith(HARDWARE_PREFIX)) { + overrideValue = element.substring(HARDWARE_PREFIX.length()); + break; + } + } + sDeviceOverrideValueMap.put(key, overrideValue); + } + return sDeviceOverrideValueMap.get(key); + } + + public static boolean isValidFraction(final float fraction) { + return fraction >= 0.0f; + } + + // {@link Resources#getDimensionPixelSize(int)} returns at least one pixel size. + public static boolean isValidDimensionPixelSize(final int dimension) { + return dimension > 0; + } + + // {@link Resources#getDimensionPixelOffset(int)} may return zero pixel offset. + public static boolean isValidDimensionPixelOffset(final int dimension) { + return dimension >= 0; + } + + public static float getFraction(final TypedArray a, final int index, final float defValue) { + final TypedValue value = a.peekValue(index); + if (value == null || !isFractionValue(value)) { + return defValue; + } + return a.getFraction(index, 1, 1, defValue); + } + + public static float getFraction(final TypedArray a, final int index) { + return getFraction(a, index, UNDEFINED_RATIO); + } + + public static int getDimensionPixelSize(final TypedArray a, final int index) { + final TypedValue value = a.peekValue(index); + if (value == null || !isDimensionValue(value)) { + return ResourceUtils.UNDEFINED_DIMENSION; + } + return a.getDimensionPixelSize(index, ResourceUtils.UNDEFINED_DIMENSION); + } + + public static float getDimensionOrFraction(TypedArray a, int index, int base, + float defValue) { + final TypedValue value = a.peekValue(index); + if (value == null) { + return defValue; + } + if (isFractionValue(value)) { + return a.getFraction(index, base, base, defValue); + } else if (isDimensionValue(value)) { + return a.getDimension(index, defValue); + } + return defValue; + } + + public static int getEnumValue(TypedArray a, int index, int defValue) { + final TypedValue value = a.peekValue(index); + if (value == null) { + return defValue; + } + if (isIntegerValue(value)) { + return a.getInt(index, defValue); + } + return defValue; + } + + public static boolean isFractionValue(TypedValue v) { + return v.type == TypedValue.TYPE_FRACTION; + } + + public static boolean isDimensionValue(TypedValue v) { + return v.type == TypedValue.TYPE_DIMENSION; + } + + public static boolean isIntegerValue(TypedValue v) { + return v.type >= TypedValue.TYPE_FIRST_INT && v.type <= TypedValue.TYPE_LAST_INT; + } + + public static boolean isStringValue(TypedValue v) { + return v.type == TypedValue.TYPE_STRING; + } +} diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java index 8b4c17322..41e59e92d 100644 --- a/java/src/com/android/inputmethod/latin/RichInputConnection.java +++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java @@ -55,7 +55,9 @@ public class RichInputConnection { public void beginBatchEdit() { if (++mNestLevel == 1) { mIC = mParent.getCurrentInputConnection(); - if (null != mIC) mIC.beginBatchEdit(); + if (null != mIC) { + mIC.beginBatchEdit(); + } } else { if (DBG) { throw new RuntimeException("Nest level too deep"); @@ -66,7 +68,9 @@ public class RichInputConnection { } public void endBatchEdit() { if (mNestLevel <= 0) Log.e(TAG, "Batch edit not in progress!"); // TODO: exception instead - if (--mNestLevel == 0 && null != mIC) mIC.endBatchEdit(); + if (--mNestLevel == 0 && null != mIC) { + mIC.endBatchEdit(); + } } private void checkBatchEdit() { @@ -79,12 +83,22 @@ public class RichInputConnection { public void finishComposingText() { checkBatchEdit(); - if (null != mIC) mIC.finishComposingText(); + if (null != mIC) { + mIC.finishComposingText(); + if (ProductionFlag.IS_EXPERIMENTAL) { + ResearchLogger.richInputConnection_finishComposingText(); + } + } } public void commitText(final CharSequence text, final int i) { checkBatchEdit(); - if (null != mIC) mIC.commitText(text, i); + if (null != mIC) { + mIC.commitText(text, i); + if (ProductionFlag.IS_EXPERIMENTAL) { + ResearchLogger.richInputConnection_commitText(text, i); + } + } } public int getCursorCapsMode(final int inputType) { @@ -107,37 +121,72 @@ public class RichInputConnection { public void deleteSurroundingText(final int i, final int j) { checkBatchEdit(); - if (null != mIC) mIC.deleteSurroundingText(i, j); + if (null != mIC) { + mIC.deleteSurroundingText(i, j); + if (ProductionFlag.IS_EXPERIMENTAL) { + ResearchLogger.richInputConnection_deleteSurroundingText(i, j); + } + } } public void performEditorAction(final int actionId) { mIC = mParent.getCurrentInputConnection(); - if (null != mIC) mIC.performEditorAction(actionId); + if (null != mIC) { + mIC.performEditorAction(actionId); + if (ProductionFlag.IS_EXPERIMENTAL) { + ResearchLogger.richInputConnection_performEditorAction(actionId); + } + } } public void sendKeyEvent(final KeyEvent keyEvent) { checkBatchEdit(); - if (null != mIC) mIC.sendKeyEvent(keyEvent); + if (null != mIC) { + mIC.sendKeyEvent(keyEvent); + if (ProductionFlag.IS_EXPERIMENTAL) { + ResearchLogger.richInputConnection_sendKeyEvent(keyEvent); + } + } } public void setComposingText(final CharSequence text, final int i) { checkBatchEdit(); - if (null != mIC) mIC.setComposingText(text, i); + if (null != mIC) { + mIC.setComposingText(text, i); + if (ProductionFlag.IS_EXPERIMENTAL) { + ResearchLogger.richInputConnection_setComposingText(text, i); + } + } } public void setSelection(final int from, final int to) { checkBatchEdit(); - if (null != mIC) mIC.setSelection(from, to); + if (null != mIC) { + mIC.setSelection(from, to); + if (ProductionFlag.IS_EXPERIMENTAL) { + ResearchLogger.richInputConnection_setSelection(from, to); + } + } } public void commitCorrection(final CorrectionInfo correctionInfo) { checkBatchEdit(); - if (null != mIC) mIC.commitCorrection(correctionInfo); + if (null != mIC) { + mIC.commitCorrection(correctionInfo); + if (ProductionFlag.IS_EXPERIMENTAL) { + ResearchLogger.richInputConnection_commitCorrection(correctionInfo); + } + } } public void commitCompletion(final CompletionInfo completionInfo) { checkBatchEdit(); - if (null != mIC) mIC.commitCompletion(completionInfo); + if (null != mIC) { + mIC.commitCompletion(completionInfo); + if (ProductionFlag.IS_EXPERIMENTAL) { + ResearchLogger.richInputConnection_commitCompletion(completionInfo); + } + } } public CharSequence getNthPreviousWord(final String sentenceSeperators, final int n) { @@ -315,9 +364,6 @@ public class RichInputConnection { if (lastOne != null && lastOne.length() == 1 && lastOne.charAt(0) == Keyboard.CODE_SPACE) { deleteSurroundingText(1, 0); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_deleteSurroundingText(1); - } } } @@ -382,13 +428,7 @@ public class RichInputConnection { return false; } deleteSurroundingText(2, 0); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_deleteSurroundingText(2); - } commitText(" ", 1); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_revertDoubleSpaceWhileInBatchEdit(); - } return true; } @@ -409,13 +449,7 @@ public class RichInputConnection { return false; } deleteSurroundingText(2, 0); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_deleteSurroundingText(2); - } commitText(" " + textBeforeCursor.subSequence(0, 1), 1); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_revertSwapPunctuation(); - } return true; } } diff --git a/java/src/com/android/inputmethod/latin/SettingsValues.java b/java/src/com/android/inputmethod/latin/SettingsValues.java index 0843bdbbc..5e355a3b8 100644 --- a/java/src/com/android/inputmethod/latin/SettingsValues.java +++ b/java/src/com/android/inputmethod/latin/SettingsValues.java @@ -185,7 +185,7 @@ public class SettingsValues { // Helper functions to create member values. private static SuggestedWords createSuggestPuncList(final String[] puncs) { - final ArrayList<SuggestedWordInfo> puncList = new ArrayList<SuggestedWordInfo>(); + final ArrayList<SuggestedWordInfo> puncList = CollectionUtils.newArrayList(); if (puncs != null) { for (final String puncSpec : puncs) { puncList.add(new SuggestedWordInfo(KeySpecParser.getLabel(puncSpec), @@ -375,8 +375,8 @@ public class SettingsValues { return volume; } - return Float.parseFloat( - Utils.getDeviceOverrideValue(res, R.array.keypress_volumes, "-1.0f")); + return Float.parseFloat(ResourceUtils.getDeviceOverrideValue( + res, R.array.keypress_volumes, "-1.0f")); } // Likewise @@ -388,8 +388,8 @@ public class SettingsValues { return ms; } - return Integer.parseInt( - Utils.getDeviceOverrideValue(res, R.array.keypress_vibration_durations, "-1")); + return Integer.parseInt(ResourceUtils.getDeviceOverrideValue( + res, R.array.keypress_vibration_durations, "-1")); } // Likewise @@ -401,7 +401,7 @@ public class SettingsValues { public static long getLastUserHistoryWriteTime( final SharedPreferences prefs, final String locale) { final String str = prefs.getString(Settings.PREF_LAST_USER_DICTIONARY_WRITE_TIME, ""); - final HashMap<String, Long> map = Utils.localeAndTimeStrToHashMap(str); + final HashMap<String, Long> map = LocaleUtils.localeAndTimeStrToHashMap(str); if (map.containsKey(locale)) { return map.get(locale); } @@ -411,12 +411,16 @@ public class SettingsValues { public static void setLastUserHistoryWriteTime( final SharedPreferences prefs, final String locale) { final String oldStr = prefs.getString(Settings.PREF_LAST_USER_DICTIONARY_WRITE_TIME, ""); - final HashMap<String, Long> map = Utils.localeAndTimeStrToHashMap(oldStr); + final HashMap<String, Long> map = LocaleUtils.localeAndTimeStrToHashMap(oldStr); map.put(locale, System.currentTimeMillis()); - final String newStr = Utils.localeAndTimeHashMapToStr(map); + final String newStr = LocaleUtils.localeAndTimeHashMapToStr(map); prefs.edit().putString(Settings.PREF_LAST_USER_DICTIONARY_WRITE_TIME, newStr).apply(); } + public boolean isSameInputType(final EditorInfo editorInfo) { + return mInputAttributes.isSameInputType(editorInfo); + } + // For debug. public String getInputAttributesDebugString() { return mInputAttributes.toString(); diff --git a/java/src/com/android/inputmethod/latin/StringUtils.java b/java/src/com/android/inputmethod/latin/StringUtils.java index 6e7d985d6..9c47a38c2 100644 --- a/java/src/com/android/inputmethod/latin/StringUtils.java +++ b/java/src/com/android/inputmethod/latin/StringUtils.java @@ -21,7 +21,7 @@ import android.text.TextUtils; import java.util.ArrayList; import java.util.Locale; -public class StringUtils { +public final class StringUtils { private StringUtils() { // This utility class is not publicly instantiable. } @@ -53,7 +53,7 @@ public class StringUtils { if (TextUtils.isEmpty(csv)) return ""; final String[] elements = csv.split(","); if (!containsInArray(key, elements)) return csv; - final ArrayList<String> result = new ArrayList<String>(elements.length - 1); + final ArrayList<String> result = CollectionUtils.newArrayList(elements.length - 1); for (final String element : elements) { if (!key.equals(element)) result.add(element); } diff --git a/java/src/com/android/inputmethod/latin/SubtypeLocale.java b/java/src/com/android/inputmethod/latin/SubtypeLocale.java index 21c9c0d1e..de5f515b0 100644 --- a/java/src/com/android/inputmethod/latin/SubtypeLocale.java +++ b/java/src/com/android/inputmethod/latin/SubtypeLocale.java @@ -45,13 +45,13 @@ public class SubtypeLocale { private static String[] sPredefinedKeyboardLayoutSet; // Keyboard layout to its display name map. private static final HashMap<String, String> sKeyboardLayoutToDisplayNameMap = - new HashMap<String, String>(); + CollectionUtils.newHashMap(); // Keyboard layout to subtype name resource id map. private static final HashMap<String, Integer> sKeyboardLayoutToNameIdsMap = - new HashMap<String, Integer>(); + CollectionUtils.newHashMap(); // Exceptional locale to subtype name resource id map. private static final HashMap<String, Integer> sExceptionalLocaleToWithLayoutNameIdsMap = - new HashMap<String, Integer>(); + CollectionUtils.newHashMap(); private static final String SUBTYPE_NAME_RESOURCE_GENERIC_PREFIX = "string/subtype_generic_"; private static final String SUBTYPE_NAME_RESOURCE_WITH_LAYOUT_PREFIX = @@ -60,11 +60,11 @@ public class SubtypeLocale { "string/subtype_no_language_"; // Exceptional locales to display name map. private static final HashMap<String, String> sExceptionalDisplayNamesMap = - new HashMap<String, String>(); + CollectionUtils.newHashMap(); // Keyboard layout set name for the subtypes that don't have a keyboardLayoutSet extra value. // This is for compatibility to keep the same subtype ids as pre-JellyBean. private static final HashMap<String,String> sLocaleAndExtraValueToKeyboardLayoutSetMap = - new HashMap<String,String>(); + CollectionUtils.newHashMap(); private SubtypeLocale() { // Intentional empty constructor for utility class. diff --git a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java index a7a5fcb5f..c693edcca 100644 --- a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java +++ b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java @@ -22,6 +22,7 @@ import android.content.Context; import android.content.Intent; import android.content.res.Configuration; import android.content.res.Resources; +import android.inputmethodservice.InputMethodService; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.AsyncTask; @@ -42,7 +43,6 @@ public class SubtypeSwitcher { private static final String TAG = SubtypeSwitcher.class.getSimpleName(); private static final SubtypeSwitcher sInstance = new SubtypeSwitcher(); - private /* final */ LatinIME mService; private /* final */ InputMethodManager mImm; private /* final */ Resources mResources; private /* final */ ConnectivityManager mConnectivityManager; @@ -68,11 +68,11 @@ public class SubtypeSwitcher { return mEnabledSubtypeCount >= 2 || !mIsSystemLanguageSameAsInputLanguage; } - public void updateEnabledSubtypeCount(int count) { + public void updateEnabledSubtypeCount(final int count) { mEnabledSubtypeCount = count; } - public void updateIsSystemLanguageSameAsInputLanguage(boolean isSame) { + public void updateIsSystemLanguageSameAsInputLanguage(final boolean isSame) { mIsSystemLanguageSameAsInputLanguage = isSame; } } @@ -81,18 +81,17 @@ public class SubtypeSwitcher { return sInstance; } - public static void init(LatinIME service) { - SubtypeLocale.init(service); - sInstance.initialize(service); - sInstance.updateAllParameters(); + public static void init(final Context context) { + SubtypeLocale.init(context); + sInstance.initialize(context); + sInstance.updateAllParameters(context); } private SubtypeSwitcher() { // Intentional empty constructor for singleton. } - private void initialize(LatinIME service) { - mService = service; + private void initialize(final Context service) { mResources = service.getResources(); mImm = ImfUtils.getInputMethodManager(service); mConnectivityManager = (ConnectivityManager) service.getSystemService( @@ -111,39 +110,46 @@ public class SubtypeSwitcher { // Update all parameters stored in SubtypeSwitcher. // Only configuration changed event is allowed to call this because this is heavy. - private void updateAllParameters() { + private void updateAllParameters(final Context context) { mCurrentSystemLocale = mResources.getConfiguration().locale; - updateSubtype(ImfUtils.getCurrentInputMethodSubtype(mService, mNoLanguageSubtype)); - updateParametersOnStartInputView(); + updateSubtype(ImfUtils.getCurrentInputMethodSubtype(context, mNoLanguageSubtype)); + updateParametersOnStartInputViewAndReturnIfCurrentSubtypeEnabled(); } - // Update parameters which are changed outside LatinIME. This parameters affect UI so they - // should be updated every time onStartInputview. - public void updateParametersOnStartInputView() { - updateEnabledSubtypes(); + /** + * Update parameters which are changed outside LatinIME. This parameters affect UI so they + * should be updated every time onStartInputView. + * + * @return true if the current subtype is enabled. + */ + public boolean updateParametersOnStartInputViewAndReturnIfCurrentSubtypeEnabled() { + final boolean currentSubtypeEnabled = + updateEnabledSubtypesAndReturnIfEnabled(mCurrentSubtype); updateShortcutIME(); + return currentSubtypeEnabled; } - // Reload enabledSubtypes from the framework. - private void updateEnabledSubtypes() { - final InputMethodSubtype currentSubtype = mCurrentSubtype; - boolean foundCurrentSubtypeBecameDisabled = true; + /** + * Update enabled subtypes from the framework. + * + * @param subtype the subtype to be checked + * @return true if the {@code subtype} is enabled. + */ + private boolean updateEnabledSubtypesAndReturnIfEnabled(final InputMethodSubtype subtype) { final List<InputMethodSubtype> enabledSubtypesOfThisIme = mImm.getEnabledInputMethodSubtypeList(null, true); - for (InputMethodSubtype ims : enabledSubtypesOfThisIme) { - if (ims.equals(currentSubtype)) { - foundCurrentSubtypeBecameDisabled = false; - } - } mNeedsToDisplayLanguage.updateEnabledSubtypeCount(enabledSubtypesOfThisIme.size()); - if (foundCurrentSubtypeBecameDisabled) { - if (DBG) { - Log.w(TAG, "Last subtype: " - + currentSubtype.getLocale() + "/" + currentSubtype.getExtraValue()); - Log.w(TAG, "Last subtype was disabled. Update to the current one."); + + for (final InputMethodSubtype ims : enabledSubtypesOfThisIme) { + if (ims.equals(subtype)) { + return true; } - updateSubtype(ImfUtils.getCurrentInputMethodSubtype(mService, mNoLanguageSubtype)); } + if (DBG) { + Log.w(TAG, "Subtype: " + subtype.getLocale() + "/" + subtype.getExtraValue() + + " was disabled"); + } + return false; } private void updateShortcutIME() { @@ -159,8 +165,8 @@ public class SubtypeSwitcher { mImm.getShortcutInputMethodsAndSubtypes(); mShortcutInputMethodInfo = null; mShortcutSubtype = null; - for (InputMethodInfo imi : shortcuts.keySet()) { - List<InputMethodSubtype> subtypes = shortcuts.get(imi); + for (final InputMethodInfo imi : shortcuts.keySet()) { + final List<InputMethodSubtype> subtypes = shortcuts.get(imi); // TODO: Returns the first found IMI for now. Should handle all shortcuts as // appropriate. mShortcutInputMethodInfo = imi; @@ -194,24 +200,24 @@ public class SubtypeSwitcher { mCurrentSubtype = newSubtype; updateShortcutIME(); - mService.onRefreshKeyboard(); } //////////////////////////// // Shortcut IME functions // //////////////////////////// - public void switchToShortcutIME() { + public void switchToShortcutIME(final InputMethodService context) { if (mShortcutInputMethodInfo == null) { return; } final String imiId = mShortcutInputMethodInfo.getId(); - switchToTargetIME(imiId, mShortcutSubtype); + switchToTargetIME(imiId, mShortcutSubtype, context); } - private void switchToTargetIME(final String imiId, final InputMethodSubtype subtype) { - final IBinder token = mService.getWindow().getWindow().getAttributes().token; + private void switchToTargetIME(final String imiId, final InputMethodSubtype subtype, + final InputMethodService context) { + final IBinder token = context.getWindow().getWindow().getAttributes().token; if (token == null) { return; } @@ -253,7 +259,7 @@ public class SubtypeSwitcher { return true; } - public void onNetworkStateChanged(Intent intent) { + public void onNetworkStateChanged(final Intent intent) { final boolean noConnection = intent.getBooleanExtra( ConnectivityManager.EXTRA_NO_CONNECTIVITY, false); mIsNetworkConnected = !noConnection; @@ -265,7 +271,7 @@ public class SubtypeSwitcher { // Subtype Switching functions // ////////////////////////////////// - public boolean needsToDisplayLanguage(Locale keyboardLocale) { + public boolean needsToDisplayLanguage(final Locale keyboardLocale) { if (keyboardLocale.toString().equals(SubtypeLocale.NO_LANGUAGE)) { return true; } @@ -279,12 +285,14 @@ public class SubtypeSwitcher { return SubtypeLocale.getSubtypeLocale(mCurrentSubtype); } - public void onConfigurationChanged(Configuration conf) { + public boolean onConfigurationChanged(final Configuration conf, final Context context) { final Locale systemLocale = conf.locale; + final boolean systemLocaleChanged = !systemLocale.equals(mCurrentSystemLocale); // If system configuration was changed, update all parameters. - if (!systemLocale.equals(mCurrentSystemLocale)) { - updateAllParameters(); + if (systemLocaleChanged) { + updateAllParameters(context); } + return systemLocaleChanged; } public InputMethodSubtype getCurrentSubtype() { diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java index 8a2341d5e..51ed09604 100644 --- a/java/src/com/android/inputmethod/latin/Suggest.java +++ b/java/src/com/android/inputmethod/latin/Suggest.java @@ -50,9 +50,8 @@ public class Suggest { private Dictionary mMainDictionary; private ContactsBinaryDictionary mContactsDict; - private WhitelistDictionary mWhiteListDictionary; private final ConcurrentHashMap<String, Dictionary> mDictionaries = - new ConcurrentHashMap<String, Dictionary>(); + CollectionUtils.newConcurrentHashMap(); public static final int MAX_SUGGESTIONS = 18; @@ -74,21 +73,11 @@ public class Suggest { mLocale = locale; mMainDictionary = mainDict; addOrReplaceDictionary(mDictionaries, Dictionary.TYPE_MAIN, mainDict); - initWhitelistAndAutocorrectAndPool(context, locale); - } - - private void initWhitelistAndAutocorrectAndPool(final Context context, final Locale locale) { - mWhiteListDictionary = new WhitelistDictionary(context, locale); - addOrReplaceDictionary(mDictionaries, Dictionary.TYPE_WHITELIST, mWhiteListDictionary); } private void initAsynchronously(final Context context, final Locale locale, final SuggestInitializationListener listener) { resetMainDict(context, locale, listener); - - // TODO: read the whitelist and init the pool asynchronously too. - // initPool should be done asynchronously now that the pool is thread-safe. - initWhitelistAndAutocorrectAndPool(context, locale); } private static void addOrReplaceDictionary( @@ -169,9 +158,17 @@ public class Suggest { public SuggestedWords getSuggestedWords( final WordComposer wordComposer, CharSequence prevWordForBigram, final ProximityInfo proximityInfo, final boolean isCorrectionEnabled) { + return getSuggestedWordsWithSessionId( + wordComposer, prevWordForBigram, proximityInfo, isCorrectionEnabled, 0); + } + + public SuggestedWords getSuggestedWordsWithSessionId( + final WordComposer wordComposer, CharSequence prevWordForBigram, + final ProximityInfo proximityInfo, final boolean isCorrectionEnabled, int sessionId) { LatinImeLogger.onStartSuggestion(prevWordForBigram); if (wordComposer.isBatchMode()) { - return getSuggestedWordsForBatchInput(wordComposer, prevWordForBigram, proximityInfo); + return getSuggestedWordsForBatchInput( + wordComposer, prevWordForBigram, proximityInfo, sessionId); } else { return getSuggestedWordsForTypingInput(wordComposer, prevWordForBigram, proximityInfo, isCorrectionEnabled); @@ -208,15 +205,6 @@ public class Suggest { wordComposerForLookup, prevWordForBigram, proximityInfo)); } - final CharSequence whitelistedWordFromWhitelistDictionary = - mWhiteListDictionary.getWhitelistedWord(consideredWord); - if (whitelistedWordFromWhitelistDictionary != null) { - // MAX_SCORE ensures this will be considered strong enough to be auto-corrected - suggestionsSet.add(new SuggestedWordInfo(whitelistedWordFromWhitelistDictionary, - SuggestedWordInfo.MAX_SCORE, SuggestedWordInfo.KIND_WHITELIST, - Dictionary.TYPE_WHITELIST)); - } - final CharSequence whitelistedWord; if (suggestionsSet.isEmpty()) { whitelistedWord = null; @@ -226,11 +214,6 @@ public class Suggest { whitelistedWord = suggestionsSet.first().mWord; } - // TODO: Change this scheme - a boolean is not enough. A whitelisted word may be "valid" - // but still autocorrected from - in the case the whitelist only capitalizes the word. - // The whitelist should be case-insensitive, so it's not possible to be consistent with - // a boolean flag. Right now this is handled with a slight hack in - // WhitelistDictionary#shouldForciblyAutoCorrectFrom. final boolean allowsToBeAutoCorrected = (null != whitelistedWord && !whitelistedWord.equals(consideredWord)) || AutoCorrection.isNotAWord(mDictionaries, consideredWord, @@ -259,7 +242,7 @@ public class Suggest { } final ArrayList<SuggestedWordInfo> suggestionsContainer = - new ArrayList<SuggestedWordInfo>(suggestionsSet); + CollectionUtils.newArrayList(suggestionsSet); final int suggestionsCount = suggestionsContainer.size(); final boolean isFirstCharCapitalized = wordComposer.isFirstCharCapitalized(); final boolean isAllUpperCase = wordComposer.isAllUpperCase(); @@ -306,29 +289,28 @@ public class Suggest { // Retrieves suggestions for the batch input. private SuggestedWords getSuggestedWordsForBatchInput( final WordComposer wordComposer, CharSequence prevWordForBigram, - final ProximityInfo proximityInfo) { + final ProximityInfo proximityInfo, int sessionId) { final BoundedTreeSet suggestionsSet = new BoundedTreeSet(sSuggestedWordInfoComparator, MAX_SUGGESTIONS); // At second character typed, search the unigrams (scores being affected by bigrams) for (final String key : mDictionaries.keySet()) { - // Skip UserUnigramDictionary and WhitelistDictionary to lookup - if (key.equals(Dictionary.TYPE_USER_HISTORY) - || key.equals(Dictionary.TYPE_WHITELIST)) { + // Skip User history dictionary for lookup + // TODO: The user history dictionary should just override getSuggestionsWithSessionId + // to make sure it doesn't return anything and we should remove this test + if (key.equals(Dictionary.TYPE_USER_HISTORY)) { continue; } final Dictionary dictionary = mDictionaries.get(key); - suggestionsSet.addAll(dictionary.getSuggestions( - wordComposer, prevWordForBigram, proximityInfo)); + suggestionsSet.addAll(dictionary.getSuggestionsWithSessionId( + wordComposer, prevWordForBigram, proximityInfo, sessionId)); } final ArrayList<SuggestedWordInfo> suggestionsContainer = - new ArrayList<SuggestedWordInfo>(suggestionsSet); + CollectionUtils.newArrayList(suggestionsSet); final int suggestionsCount = suggestionsContainer.size(); - final boolean isFirstCharCapitalized = wordComposer.isAutoCapitalized(); - // TODO: Handle the manual temporary shifted mode. - // TODO: Should handle TextUtils.CAP_MODE_CHARACTER. - final boolean isAllUpperCase = false; + final boolean isFirstCharCapitalized = wordComposer.wasShiftedNoLock(); + final boolean isAllUpperCase = wordComposer.isAllUpperCase(); if (isFirstCharCapitalized || isAllUpperCase) { for (int i = 0; i < suggestionsCount; ++i) { final SuggestedWordInfo wordInfo = suggestionsContainer.get(i); @@ -356,7 +338,7 @@ public class Suggest { typedWordInfo.setDebugString("+"); final int suggestionsSize = suggestions.size(); final ArrayList<SuggestedWordInfo> suggestionsList = - new ArrayList<SuggestedWordInfo>(suggestionsSize); + CollectionUtils.newArrayList(suggestionsSize); suggestionsList.add(typedWordInfo); // Note: i here is the index in mScores[], but the index in mSuggestions is one more // than i because we added the typed word to mSuggestions without touching mScores. @@ -409,7 +391,7 @@ public class Suggest { } public void close() { - final HashSet<Dictionary> dictionaries = new HashSet<Dictionary>(); + final HashSet<Dictionary> dictionaries = CollectionUtils.newHashSet(); dictionaries.addAll(mDictionaries.values()); for (final Dictionary dictionary : dictionaries) { dictionary.close(); diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java index 88fc006df..68ecfa0d7 100644 --- a/java/src/com/android/inputmethod/latin/SuggestedWords.java +++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java @@ -24,8 +24,10 @@ import java.util.Arrays; import java.util.HashSet; public class SuggestedWords { + private static final ArrayList<SuggestedWordInfo> EMPTY_WORD_INFO_LIST = + CollectionUtils.newArrayList(0); public static final SuggestedWords EMPTY = new SuggestedWords( - new ArrayList<SuggestedWordInfo>(0), false, false, false, false, false); + EMPTY_WORD_INFO_LIST, false, false, false, false, false); public final boolean mTypedWordValid; // Note: this INCLUDES cases where the word will auto-correct to itself. A good definition @@ -83,7 +85,7 @@ public class SuggestedWords { public static ArrayList<SuggestedWordInfo> getFromApplicationSpecifiedCompletions( final CompletionInfo[] infos) { - final ArrayList<SuggestedWordInfo> result = new ArrayList<SuggestedWordInfo>(); + final ArrayList<SuggestedWordInfo> result = CollectionUtils.newArrayList(); for (CompletionInfo info : infos) { if (null != info && info.getText() != null) { result.add(new SuggestedWordInfo(info.getText(), SuggestedWordInfo.MAX_SCORE, @@ -97,8 +99,8 @@ public class SuggestedWords { // and replace it with what the user currently typed. public static ArrayList<SuggestedWordInfo> getTypedWordAndPreviousSuggestions( final CharSequence typedWord, final SuggestedWords previousSuggestions) { - final ArrayList<SuggestedWordInfo> suggestionsList = new ArrayList<SuggestedWordInfo>(); - final HashSet<String> alreadySeen = new HashSet<String>(); + final ArrayList<SuggestedWordInfo> suggestionsList = CollectionUtils.newArrayList(); + final HashSet<String> alreadySeen = CollectionUtils.newHashSet(); suggestionsList.add(new SuggestedWordInfo(typedWord, SuggestedWordInfo.MAX_SCORE, SuggestedWordInfo.KIND_TYPED, Dictionary.TYPE_USER_TYPED)); alreadySeen.add(typedWord.toString()); diff --git a/java/src/com/android/inputmethod/latin/UserHistoryDictIOUtils.java b/java/src/com/android/inputmethod/latin/UserHistoryDictIOUtils.java new file mode 100644 index 000000000..942c82837 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/UserHistoryDictIOUtils.java @@ -0,0 +1,193 @@ +/* + * 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.latin; + +import android.util.Log; + +import com.android.inputmethod.latin.makedict.BinaryDictInputOutput; +import com.android.inputmethod.latin.makedict.BinaryDictInputOutput.FusionDictionaryBufferInterface; +import com.android.inputmethod.latin.makedict.FusionDictionary; +import com.android.inputmethod.latin.makedict.FusionDictionary.Node; +import com.android.inputmethod.latin.makedict.PendingAttribute; +import com.android.inputmethod.latin.makedict.UnsupportedFormatException; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +/** + * Reads and writes Binary files for a UserHistoryDictionary. + * + * All the methods in this class are static. + */ +public class UserHistoryDictIOUtils { + private static final String TAG = UserHistoryDictIOUtils.class.getSimpleName(); + private static final boolean DEBUG = false; + + public interface OnAddWordListener { + public void setUnigram(final String word, final String shortcutTarget, final int frequency); + public void setBigram(final String word1, final String word2, final int frequency); + } + + public interface BigramDictionaryInterface { + public int getFrequency(final String word1, final String word2); + } + + public static final class ByteArrayWrapper implements FusionDictionaryBufferInterface { + private byte[] mBuffer; + private int mPosition; + + ByteArrayWrapper(final byte[] buffer) { + mBuffer = buffer; + mPosition = 0; + } + + @Override + public int readUnsignedByte() { + return ((int)mBuffer[mPosition++]) & 0xFF; + } + + @Override + public int readUnsignedShort() { + final int retval = readUnsignedByte(); + return (retval << 8) + readUnsignedByte(); + } + + @Override + public int readUnsignedInt24() { + final int retval = readUnsignedShort(); + return (retval << 8) + readUnsignedByte(); + } + + @Override + public int readInt() { + final int retval = readUnsignedShort(); + return (retval << 16) + readUnsignedShort(); + } + + @Override + public int position() { + return mPosition; + } + + @Override + public void position(int position) { + mPosition = position; + } + } + + /** + * Writes dictionary to file. + */ + public static void writeDictionaryBinary(final OutputStream destination, + final BigramDictionaryInterface dict, final UserHistoryDictionaryBigramList bigrams, + final int version) { + + final FusionDictionary fusionDict = constructFusionDictionary(dict, bigrams); + + try { + BinaryDictInputOutput.writeDictionaryBinary(destination, fusionDict, version); + } catch (IOException e) { + Log.e(TAG, "IO exception while writing file: " + e); + } catch (UnsupportedFormatException e) { + Log.e(TAG, "Unsupported fomat: " + e); + } + } + + /** + * Constructs a new FusionDictionary from BigramDictionaryInterface. + */ + /* packages for test */ static FusionDictionary constructFusionDictionary( + final BigramDictionaryInterface dict, final UserHistoryDictionaryBigramList bigrams) { + + final FusionDictionary fusionDict = new FusionDictionary(new Node(), + new FusionDictionary.DictionaryOptions( + new HashMap<String,String>(), false, false)); + + for (final String word1 : bigrams.keySet()) { + final HashMap<String, Byte> word1Bigrams = bigrams.getBigrams(word1); + for (final String word2 : word1Bigrams.keySet()) { + final int freq = dict.getFrequency(word1, word2); + + if (DEBUG) { + if (word1 == null) { + Log.d(TAG, "add unigram: " + word2 + "," + Integer.toString(freq)); + } else { + Log.d(TAG, "add bigram: " + word1 + + "," + word2 + "," + Integer.toString(freq)); + } + } + + if (word1 == null) { // unigram + fusionDict.add(word2, freq, null, false /* isNotAWord */); + } else { // bigram + fusionDict.setBigram(word1, word2, freq); + } + bigrams.updateBigram(word1, word2, (byte)freq); + } + } + + return fusionDict; + } + + /** + * Reads dictionary from file. + */ + public static void readDictionaryBinary(final FusionDictionaryBufferInterface buffer, + final OnAddWordListener dict) { + final Map<Integer, String> unigrams = CollectionUtils.newTreeMap(); + final Map<Integer, Integer> frequencies = CollectionUtils.newTreeMap(); + final Map<Integer, ArrayList<PendingAttribute>> bigrams = CollectionUtils.newTreeMap(); + + try { + BinaryDictInputOutput.readUnigramsAndBigramsBinary(buffer, unigrams, frequencies, + bigrams); + addWordsFromWordMap(unigrams, frequencies, bigrams, dict); + } catch (IOException e) { + Log.e(TAG, "IO exception while reading file: " + e); + } catch (UnsupportedFormatException e) { + Log.e(TAG, "Unsupported format: " + e); + } + } + + /** + * Adds all unigrams and bigrams in maps to OnAddWordListener. + */ + /* package for test */ static void addWordsFromWordMap(final Map<Integer, String> unigrams, + final Map<Integer, Integer> frequencies, + final Map<Integer, ArrayList<PendingAttribute>> bigrams, final OnAddWordListener to) { + + for (Map.Entry<Integer, String> entry : unigrams.entrySet()) { + final String word1 = entry.getValue(); + final int unigramFrequency = frequencies.get(entry.getKey()); + to.setUnigram(word1, null, unigramFrequency); + + final ArrayList<PendingAttribute> attrList = bigrams.get(entry.getKey()); + + if (attrList != null) { + for (final PendingAttribute attr : attrList) { + to.setBigram(word1, unigrams.get(attr.mAddress), + BinaryDictInputOutput.reconstructBigramFrequency(unigramFrequency, + attr.mFrequency)); + } + } + } + + } +}
\ No newline at end of file diff --git a/java/src/com/android/inputmethod/latin/UserHistoryDictionary.java b/java/src/com/android/inputmethod/latin/UserHistoryDictionary.java index 3bb670c9a..6c9d1c250 100644 --- a/java/src/com/android/inputmethod/latin/UserHistoryDictionary.java +++ b/java/src/com/android/inputmethod/latin/UserHistoryDictionary.java @@ -52,14 +52,14 @@ public class UserHistoryDictionary extends ExpandableDictionary { private static final int FREQUENCY_FOR_TYPED = 2; /** Maximum number of pairs. Pruning will start when databases goes above this number. */ - private static int sMaxHistoryBigrams = 10000; + public static final int sMaxHistoryBigrams = 10000; /** * When it hits maximum bigram pair, it will delete until you are left with * only (sMaxHistoryBigrams - sDeleteHistoryBigrams) pairs. * Do not keep this number small to avoid deleting too often. */ - private static int sDeleteHistoryBigrams = 1000; + public static final int sDeleteHistoryBigrams = 1000; /** * Database version should increase if the database structure changes @@ -93,10 +93,10 @@ public class UserHistoryDictionary extends ExpandableDictionary { private final static HashMap<String, String> sDictProjectionMap; private final static ConcurrentHashMap<String, SoftReference<UserHistoryDictionary>> - sLangDictCache = new ConcurrentHashMap<String, SoftReference<UserHistoryDictionary>>(); + sLangDictCache = CollectionUtils.newConcurrentHashMap(); static { - sDictProjectionMap = new HashMap<String, String>(); + sDictProjectionMap = CollectionUtils.newHashMap(); sDictProjectionMap.put(MAIN_COLUMN_ID, MAIN_COLUMN_ID); sDictProjectionMap.put(MAIN_COLUMN_WORD1, MAIN_COLUMN_WORD1); sDictProjectionMap.put(MAIN_COLUMN_WORD2, MAIN_COLUMN_WORD2); @@ -109,12 +109,8 @@ public class UserHistoryDictionary extends ExpandableDictionary { private static DatabaseHelper sOpenHelper = null; - public void setDatabaseMax(int maxHistoryBigram) { - sMaxHistoryBigrams = maxHistoryBigram; - } - - public void setDatabaseDelete(int deleteHistoryBigram) { - sDeleteHistoryBigrams = deleteHistoryBigram; + public String getLocale() { + return mLocale; } public synchronized static UserHistoryDictionary getInstance( @@ -502,9 +498,11 @@ public class UserHistoryDictionary extends ExpandableDictionary { needsToSave(fc, isValid, addLevel0Bigram)) { freq = fc; } else { + // Delete this entry freq = -1; } } else { + // Delete this entry freq = -1; } } @@ -541,6 +539,7 @@ public class UserHistoryDictionary extends ExpandableDictionary { getContentValues(word1, word2, mLocale)); pairId = pairIdLong.intValue(); } + // Eliminate freq == 0 because that word is profanity. if (freq > 0) { if (PROFILE_SAVE_RESTORE) { ++profInsert; diff --git a/java/src/com/android/inputmethod/latin/UserHistoryDictionaryBigramList.java b/java/src/com/android/inputmethod/latin/UserHistoryDictionaryBigramList.java index 610652ac1..bb0f5429a 100644 --- a/java/src/com/android/inputmethod/latin/UserHistoryDictionaryBigramList.java +++ b/java/src/com/android/inputmethod/latin/UserHistoryDictionaryBigramList.java @@ -29,9 +29,8 @@ import java.util.Set; public class UserHistoryDictionaryBigramList { public static final byte FORGETTING_CURVE_INITIAL_VALUE = 0; private static final String TAG = UserHistoryDictionaryBigramList.class.getSimpleName(); - private static final HashMap<String, Byte> EMPTY_BIGRAM_MAP = new HashMap<String, Byte>(); - private final HashMap<String, HashMap<String, Byte>> mBigramMap = - new HashMap<String, HashMap<String, Byte>>(); + private static final HashMap<String, Byte> EMPTY_BIGRAM_MAP = CollectionUtils.newHashMap(); + private final HashMap<String, HashMap<String, Byte>> mBigramMap = CollectionUtils.newHashMap(); private int mSize = 0; public void evictAll() { @@ -57,7 +56,7 @@ public class UserHistoryDictionaryBigramList { if (mBigramMap.containsKey(word1)) { map = mBigramMap.get(word1); } else { - map = new HashMap<String, Byte>(); + map = CollectionUtils.newHashMap(); mBigramMap.put(word1, map); } if (!map.containsKey(word2)) { diff --git a/java/src/com/android/inputmethod/latin/UserHistoryForgettingCurveUtils.java b/java/src/com/android/inputmethod/latin/UserHistoryForgettingCurveUtils.java index 5a2fdf48e..3d3bd980c 100644 --- a/java/src/com/android/inputmethod/latin/UserHistoryForgettingCurveUtils.java +++ b/java/src/com/android/inputmethod/latin/UserHistoryForgettingCurveUtils.java @@ -19,7 +19,7 @@ package com.android.inputmethod.latin; import android.text.format.DateUtils; import android.util.Log; -public class UserHistoryForgettingCurveUtils { +public final class UserHistoryForgettingCurveUtils { private static final String TAG = UserHistoryForgettingCurveUtils.class.getSimpleName(); private static final boolean DEBUG = false; private static final int FC_FREQ_MAX = 127; diff --git a/java/src/com/android/inputmethod/latin/Utils.java b/java/src/com/android/inputmethod/latin/Utils.java index c6b5c338b..1c98b92cd 100644 --- a/java/src/com/android/inputmethod/latin/Utils.java +++ b/java/src/com/android/inputmethod/latin/Utils.java @@ -16,20 +16,16 @@ package com.android.inputmethod.latin; -import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; -import android.content.res.Resources; import android.inputmethodservice.InputMethodService; import android.net.Uri; import android.os.AsyncTask; -import android.os.Build; import android.os.Environment; import android.os.Handler; import android.os.HandlerThread; import android.os.Process; import android.text.TextUtils; -import android.text.format.DateUtils; import android.util.Log; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; @@ -45,9 +41,8 @@ import java.io.PrintWriter; import java.nio.channels.FileChannel; import java.text.SimpleDateFormat; import java.util.Date; -import java.util.HashMap; -public class Utils { +public final class Utils { private Utils() { // This utility class is not publicly instantiable. } @@ -65,44 +60,6 @@ public class Utils { } } - public static class GCUtils { - private static final String GC_TAG = GCUtils.class.getSimpleName(); - public static final int GC_TRY_COUNT = 2; - // GC_TRY_LOOP_MAX is used for the hard limit of GC wait, - // GC_TRY_LOOP_MAX should be greater than GC_TRY_COUNT. - public static final int GC_TRY_LOOP_MAX = 5; - private static final long GC_INTERVAL = DateUtils.SECOND_IN_MILLIS; - private static GCUtils sInstance = new GCUtils(); - private int mGCTryCount = 0; - - public static GCUtils getInstance() { - return sInstance; - } - - public void reset() { - mGCTryCount = 0; - } - - public boolean tryGCOrWait(String metaData, Throwable t) { - if (mGCTryCount == 0) { - System.gc(); - } - if (++mGCTryCount > GC_TRY_COUNT) { - LatinImeLogger.logOnException(metaData, t); - return false; - } else { - try { - Thread.sleep(GC_INTERVAL); - return true; - } catch (InterruptedException e) { - Log.e(GC_TAG, "Sleep was interrupted."); - LatinImeLogger.logOnException(metaData, t); - return false; - } - } - } - } - /* package */ static class RingCharBuffer { private static RingCharBuffer sRingCharBuffer = new RingCharBuffer(); private static final char PLACEHOLDER_DELIMITER_CHAR = '\uFFFC'; @@ -222,7 +179,7 @@ public class Utils { return getStackTrace(Integer.MAX_VALUE - 1); } - public static class UsabilityStudyLogUtils { + public static final class UsabilityStudyLogUtils { // TODO: remove code duplication with ResearchLog class private static final String USABILITY_TAG = UsabilityStudyLogUtils.class.getSimpleName(); private static final String FILENAME = "log.txt"; @@ -431,34 +388,38 @@ public class Utils { } } - public static float getDipScale(Context context) { - final float scale = context.getResources().getDisplayMetrics().density; - return scale; - } - - /** Convert pixel to DIP */ - public static int dipToPixel(float scale, int dip) { - return (int) (dip * scale + 0.5); - } - - public static class Stats { + public static final class Stats { public static void onNonSeparator(final char code, final int x, final int y) { RingCharBuffer.getInstance().push(code, x, y); LatinImeLogger.logOnInputChar(); } - public static void onSeparator(final int code, final int x, - final int y) { - // TODO: accept code points - RingCharBuffer.getInstance().push((char)code, x, y); + public static void onSeparator(final int code, final int x, final int y) { + // Helper method to log a single code point separator + // TODO: cache this mapping of a code point to a string in a sparse array in StringUtils + onSeparator(new String(new int[]{code}, 0, 1), x, y); + } + + public static void onSeparator(final String separator, final int x, final int y) { + final int length = separator.length(); + for (int i = 0; i < length; i = Character.offsetByCodePoints(separator, i, 1)) { + int codePoint = Character.codePointAt(separator, i); + // TODO: accept code points + RingCharBuffer.getInstance().push((char)codePoint, x, y); + } LatinImeLogger.logOnInputSeparator(); } public static void onAutoCorrection(final String typedWord, final String correctedWord, - final int separatorCode) { + final String separatorString) { if (TextUtils.isEmpty(typedWord)) return; - LatinImeLogger.logOnAutoCorrection(typedWord, correctedWord, separatorCode); + // TODO: this fails when the separator is more than 1 code point long, but + // the backend can't handle it yet. The only case when this happens is with + // smileys and other multi-character keys. + final int codePoint = TextUtils.isEmpty(separatorString) ? Constants.NOT_A_CODE + : separatorString.codePointAt(0); + LatinImeLogger.logOnAutoCorrection(typedWord, correctedWord, codePoint); } public static void onAutoCorrectionCancellation() { @@ -474,60 +435,4 @@ public class Utils { if (TextUtils.isEmpty(info)) return null; return info; } - - private static final String HARDWARE_PREFIX = Build.HARDWARE + ","; - private static final HashMap<String, String> sDeviceOverrideValueMap = - new HashMap<String, String>(); - - public static String getDeviceOverrideValue(Resources res, int overrideResId, String defValue) { - final int orientation = res.getConfiguration().orientation; - final String key = overrideResId + "-" + orientation; - if (!sDeviceOverrideValueMap.containsKey(key)) { - String overrideValue = defValue; - for (final String element : res.getStringArray(overrideResId)) { - if (element.startsWith(HARDWARE_PREFIX)) { - overrideValue = element.substring(HARDWARE_PREFIX.length()); - break; - } - } - sDeviceOverrideValueMap.put(key, overrideValue); - } - return sDeviceOverrideValueMap.get(key); - } - - private static final HashMap<String, Long> EMPTY_LT_HASH_MAP = new HashMap<String, Long>(); - private static final String LOCALE_AND_TIME_STR_SEPARATER = ","; - public static HashMap<String, Long> localeAndTimeStrToHashMap(String str) { - if (TextUtils.isEmpty(str)) { - return EMPTY_LT_HASH_MAP; - } - final String[] ss = str.split(LOCALE_AND_TIME_STR_SEPARATER); - final int N = ss.length; - if (N < 2 || N % 2 != 0) { - return EMPTY_LT_HASH_MAP; - } - final HashMap<String, Long> retval = new HashMap<String, Long>(); - for (int i = 0; i < N / 2; ++i) { - final String localeStr = ss[i * 2]; - final long time = Long.valueOf(ss[i * 2 + 1]); - retval.put(localeStr, time); - } - return retval; - } - - public static String localeAndTimeHashMapToStr(HashMap<String, Long> map) { - if (map == null || map.isEmpty()) { - return ""; - } - final StringBuilder builder = new StringBuilder(); - for (String localeStr : map.keySet()) { - if (builder.length() > 0) { - builder.append(LOCALE_AND_TIME_STR_SEPARATER); - } - final Long time = map.get(localeStr); - builder.append(localeStr).append(LOCALE_AND_TIME_STR_SEPARATER); - builder.append(String.valueOf(time)); - } - return builder.toString(); - } } diff --git a/java/src/com/android/inputmethod/latin/VibratorUtils.java b/java/src/com/android/inputmethod/latin/VibratorUtils.java index 33ffdd9c9..b6696cec0 100644 --- a/java/src/com/android/inputmethod/latin/VibratorUtils.java +++ b/java/src/com/android/inputmethod/latin/VibratorUtils.java @@ -19,7 +19,7 @@ package com.android.inputmethod.latin; import android.content.Context; import android.os.Vibrator; -public class VibratorUtils { +public final class VibratorUtils { private static final VibratorUtils sInstance = new VibratorUtils(); private Vibrator mVibrator; diff --git a/java/src/com/android/inputmethod/latin/WhitelistDictionary.java b/java/src/com/android/inputmethod/latin/WhitelistDictionary.java deleted file mode 100644 index 14476dcf0..000000000 --- a/java/src/com/android/inputmethod/latin/WhitelistDictionary.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright (C) 2011 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.text.TextUtils; -import android.util.Log; -import android.util.Pair; - -import com.android.inputmethod.keyboard.ProximityInfo; -import com.android.inputmethod.latin.LocaleUtils.RunInLocale; -import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Locale; - -public class WhitelistDictionary extends ExpandableDictionary { - - private static final boolean DBG = LatinImeLogger.sDBG; - private static final String TAG = WhitelistDictionary.class.getSimpleName(); - - private final HashMap<String, Pair<Integer, String>> mWhitelistWords = - new HashMap<String, Pair<Integer, String>>(); - - // TODO: Conform to the async load contact of ExpandableDictionary - public WhitelistDictionary(final Context context, final Locale locale) { - super(context, Dictionary.TYPE_WHITELIST); - // TODO: Move whitelist dictionary into main dictionary. - final RunInLocale<Void> job = new RunInLocale<Void>() { - @Override - protected Void job(Resources res) { - initWordlist(res.getStringArray(R.array.wordlist_whitelist)); - return null; - } - }; - job.runInLocale(context.getResources(), locale); - } - - private void initWordlist(String[] wordlist) { - mWhitelistWords.clear(); - final int N = wordlist.length; - if (N % 3 != 0) { - if (DBG) { - Log.d(TAG, "The number of the whitelist is invalid."); - } - return; - } - try { - for (int i = 0; i < N; i += 3) { - final int score = Integer.valueOf(wordlist[i]); - final String before = wordlist[i + 1]; - final String after = wordlist[i + 2]; - if (before != null && after != null) { - mWhitelistWords.put( - before.toLowerCase(), new Pair<Integer, String>(score, after)); - addWord(after, null /* shortcut */, score); - } - } - } catch (NumberFormatException e) { - if (DBG) { - Log.d(TAG, "The score of the word is invalid."); - } - } - } - - public String getWhitelistedWord(String before) { - if (before == null) return null; - final String lowerCaseBefore = before.toLowerCase(); - if(mWhitelistWords.containsKey(lowerCaseBefore)) { - if (DBG) { - Log.d(TAG, "--- found whitelistedWord: " + lowerCaseBefore); - } - return mWhitelistWords.get(lowerCaseBefore).second; - } - return null; - } - - @Override - public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer, - final CharSequence prevWord, final ProximityInfo proximityInfo) { - // Whitelist does not supply any suggestions or predictions. - return null; - } - - // See LatinIME#updateSuggestions. This breaks in the (queer) case that the whitelist - // lists that word a should autocorrect to word b, and word c would autocorrect to - // an upper-cased version of a. In this case, the way this return value is used would - // remove the first candidate when the user typed the upper-cased version of A. - // Example : abc -> def and xyz -> Abc - // A user typing Abc would experience it being autocorrected to something else (not - // necessarily def). - // There is no such combination in the whitelist at the time and there probably won't - // ever be - it doesn't make sense. But still. - public boolean shouldForciblyAutoCorrectFrom(CharSequence word) { - if (TextUtils.isEmpty(word)) return false; - final String correction = getWhitelistedWord(word.toString()); - if (TextUtils.isEmpty(correction)) return false; - return !correction.equals(word); - } - - // Leave implementation of getWords and isValidWord to the superclass. - // The words have been added to the ExpandableDictionary with addWord() inside initWordlist. -} diff --git a/java/src/com/android/inputmethod/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java index 5606a58e4..4b7adf26b 100644 --- a/java/src/com/android/inputmethod/latin/WordComposer.java +++ b/java/src/com/android/inputmethod/latin/WordComposer.java @@ -17,7 +17,6 @@ package com.android.inputmethod.latin; import com.android.inputmethod.keyboard.Key; -import com.android.inputmethod.keyboard.KeyDetector; import com.android.inputmethod.keyboard.Keyboard; import java.util.Arrays; @@ -26,12 +25,16 @@ import java.util.Arrays; * A place to store the currently composing word with information such as adjacent key codes as well */ public class WordComposer { - - public static final int NOT_A_CODE = KeyDetector.NOT_A_CODE; - public static final int NOT_A_COORDINATE = -1; - private static final int N = BinaryDictionary.MAX_WORD_LENGTH; + public static final int CAPS_MODE_OFF = 0; + // 1 is shift bit, 2 is caps bit, 4 is auto bit but this is just a convention as these bits + // aren't used anywhere in the code + public static final int CAPS_MODE_MANUAL_SHIFTED = 0x1; + public static final int CAPS_MODE_MANUAL_SHIFT_LOCKED = 0x3; + public static final int CAPS_MODE_AUTO_SHIFTED = 0x5; + public static final int CAPS_MODE_AUTO_SHIFT_LOCKED = 0x7; + private int[] mPrimaryKeyCodes; private final InputPointers mInputPointers = new InputPointers(N); private final StringBuilder mTypedWord; @@ -42,7 +45,7 @@ public class WordComposer { // Cache these values for performance private int mCapsCount; private int mDigitsCount; - private boolean mAutoCapitalized; + private int mCapitalizedMode; private int mTrailingSingleQuotesCount; private int mCodePointSize; @@ -68,7 +71,7 @@ public class WordComposer { mCapsCount = source.mCapsCount; mDigitsCount = source.mDigitsCount; mIsFirstCharCapitalized = source.mIsFirstCharCapitalized; - mAutoCapitalized = source.mAutoCapitalized; + mCapitalizedMode = source.mCapitalizedMode; mTrailingSingleQuotesCount = source.mTrailingSingleQuotesCount; mIsResumed = source.mIsResumed; mIsBatchMode = source.mIsBatchMode; @@ -166,7 +169,7 @@ public class WordComposer { final int codePoint = Character.codePointAt(word, i); // We don't want to override the batch input points that are held in mInputPointers // (See {@link #add(int,int,int)}). - add(codePoint, NOT_A_COORDINATE, NOT_A_COORDINATE); + add(codePoint, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); } } @@ -181,7 +184,7 @@ public class WordComposer { add(codePoint, x, y); return; } - add(codePoint, NOT_A_COORDINATE, NOT_A_COORDINATE); + add(codePoint, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); } /** @@ -262,7 +265,14 @@ public class WordComposer { * @return true if all user typed chars are upper case, false otherwise */ public boolean isAllUpperCase() { - return (mCapsCount > 0) && (mCapsCount == size()); + return mCapitalizedMode == CAPS_MODE_AUTO_SHIFT_LOCKED + || mCapitalizedMode == CAPS_MODE_MANUAL_SHIFT_LOCKED + || (mCapsCount > 0) && (mCapsCount == size()); + } + + public boolean wasShiftedNoLock() { + return mCapitalizedMode == CAPS_MODE_AUTO_SHIFTED + || mCapitalizedMode == CAPS_MODE_MANUAL_SHIFTED; } /** @@ -280,20 +290,27 @@ public class WordComposer { } /** - * Saves the reason why the word is capitalized - whether it was automatic or - * due to the user hitting shift in the middle of a sentence. - * @param auto whether it was an automatic capitalization due to start of sentence + * Saves the caps mode at the start of composing. + * + * WordComposer needs to know about this for several reasons. The first is, we need to know + * after the fact what the reason was, to register the correct form into the user history + * dictionary: if the word was automatically capitalized, we should insert it in all-lower + * case but if it's a manual pressing of shift, then it should be inserted as is. + * Also, batch input needs to know about the current caps mode to display correctly + * capitalized suggestions. + * @param mode the mode at the time of start */ - public void setAutoCapitalized(boolean auto) { - mAutoCapitalized = auto; + public void setCapitalizedModeAtStartComposingTime(final int mode) { + mCapitalizedMode = mode; } /** * Returns whether the word was automatically capitalized. * @return whether the word was automatically capitalized */ - public boolean isAutoCapitalized() { - return mAutoCapitalized; + public boolean wasAutoCapitalized() { + return mCapitalizedMode == CAPS_MODE_AUTO_SHIFT_LOCKED + || mCapitalizedMode == CAPS_MODE_AUTO_SHIFTED; } /** @@ -319,14 +336,14 @@ public class WordComposer { // `type' should be one of the LastComposedWord.COMMIT_TYPE_* constants above. public LastComposedWord commitWord(final int type, final String committedWord, - final int separatorCode, final CharSequence prevWord) { + final String separatorString, final CharSequence prevWord) { // Note: currently, we come here whenever we commit a word. If it's a MANUAL_PICK // or a DECIDED_WORD we may cancel the commit later; otherwise, we should deactivate // the last composed word to ensure this does not happen. final int[] primaryKeyCodes = mPrimaryKeyCodes; mPrimaryKeyCodes = new int[N]; final LastComposedWord lastComposedWord = new LastComposedWord(primaryKeyCodes, - mInputPointers, mTypedWord.toString(), committedWord, separatorCode, + mInputPointers, mTypedWord.toString(), committedWord, separatorString, prevWord); mInputPointers.reset(); if (type != LastComposedWord.COMMIT_TYPE_DECIDED_WORD diff --git a/java/src/com/android/inputmethod/latin/XmlParseUtils.java b/java/src/com/android/inputmethod/latin/XmlParseUtils.java index 481cdfa47..b5cbaf19e 100644 --- a/java/src/com/android/inputmethod/latin/XmlParseUtils.java +++ b/java/src/com/android/inputmethod/latin/XmlParseUtils.java @@ -23,7 +23,7 @@ import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; -public class XmlParseUtils { +public final class XmlParseUtils { private XmlParseUtils() { // This utility class is not publicly instantiable. } diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java index 2c3eee74c..abc39d923 100644 --- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java +++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java @@ -22,15 +22,19 @@ import com.android.inputmethod.latin.makedict.FusionDictionary.Node; import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString; import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.OutputStream; -import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; import java.util.Map; +import java.util.Stack; import java.util.TreeMap; /** @@ -52,6 +56,8 @@ public class BinaryDictInputOutput { * s | has a terminal ? 1 bit, 1 = yes, 0 = no : FLAG_IS_TERMINAL * | has shortcut targets ? 1 bit, 1 = yes, 0 = no : FLAG_HAS_SHORTCUT_TARGETS * | has bigrams ? 1 bit, 1 = yes, 0 = no : FLAG_HAS_BIGRAMS + * | is not a word ? 1 bit, 1 = yes, 0 = no : FLAG_IS_NOT_A_WORD + * | is blacklisted ? 1 bit, 1 = yes, 0 = no : FLAG_IS_BLACKLISTED * * c | IF FLAG_HAS_MULTIPLE_CHARS * h | char, char, char, char n * (1 or 3 bytes) : use CharGroupInfo for i/o helpers @@ -124,7 +130,7 @@ public class BinaryDictInputOutput { */ private static final int VERSION_1_MAGIC_NUMBER = 0x78B1; - private static final int VERSION_2_MAGIC_NUMBER = 0x9BC13AFE; + public static final int VERSION_2_MAGIC_NUMBER = 0x9BC13AFE; private static final int MINIMUM_SUPPORTED_VERSION = 1; private static final int MAXIMUM_SUPPORTED_VERSION = 2; private static final int NOT_A_VERSION_NUMBER = -1; @@ -150,6 +156,8 @@ public class BinaryDictInputOutput { private static final int FLAG_IS_TERMINAL = 0x10; private static final int FLAG_HAS_SHORTCUT_TARGETS = 0x08; private static final int FLAG_HAS_BIGRAMS = 0x04; + private static final int FLAG_IS_NOT_A_WORD = 0x02; + private static final int FLAG_IS_BLACKLISTED = 0x01; private static final int FLAG_ATTRIBUTE_HAS_NEXT = 0x80; private static final int FLAG_ATTRIBUTE_OFFSET_NEGATIVE = 0x40; @@ -185,6 +193,54 @@ public class BinaryDictInputOutput { // suspicion that a bug might be causing an infinite loop. private static final int MAX_PASSES = 24; + public interface FusionDictionaryBufferInterface { + public int readUnsignedByte(); + public int readUnsignedShort(); + public int readUnsignedInt24(); + public int readInt(); + public int position(); + public void position(int newPosition); + } + + public static final class ByteBufferWrapper implements FusionDictionaryBufferInterface { + private ByteBuffer mBuffer; + + public ByteBufferWrapper(final ByteBuffer buffer) { + mBuffer = buffer; + } + + @Override + public int readUnsignedByte() { + return ((int)mBuffer.get()) & 0xFF; + } + + @Override + public int readUnsignedShort() { + return ((int)mBuffer.getShort()) & 0xFFFF; + } + + @Override + public int readUnsignedInt24() { + final int retval = readUnsignedByte(); + return (retval << 16) + readUnsignedShort(); + } + + @Override + public int readInt() { + return mBuffer.getInt(); + } + + @Override + public int position() { + return mBuffer.position(); + } + + @Override + public void position(int newPos) { + mBuffer.position(newPos); + } + } + /** * A class grouping utility function for our specific character encoding. */ @@ -307,33 +363,32 @@ public class BinaryDictInputOutput { } /** - * Reads a string from a RandomAccessFile. This is the converse of the above method. + * Reads a string from a buffer. This is the converse of the above method. */ - private static String readString(final RandomAccessFile source) throws IOException { + private static String readString(final FusionDictionaryBufferInterface buffer) { final StringBuilder s = new StringBuilder(); - int character = readChar(source); + int character = readChar(buffer); while (character != INVALID_CHARACTER) { s.appendCodePoint(character); - character = readChar(source); + character = readChar(buffer); } return s.toString(); } /** - * Reads a character from the file. + * Reads a character from the buffer. * * This follows the character format documented earlier in this source file. * - * @param source the file, positioned over an encoded character. + * @param buffer the buffer, positioned over an encoded character. * @return the character code. */ - private static int readChar(RandomAccessFile source) throws IOException { - int character = source.readUnsignedByte(); + private static int readChar(final FusionDictionaryBufferInterface buffer) { + int character = buffer.readUnsignedByte(); if (!fitsOnOneByte(character)) { - if (GROUP_CHARACTERS_TERMINATOR == character) - return INVALID_CHARACTER; + if (GROUP_CHARACTERS_TERMINATOR == character) return INVALID_CHARACTER; character <<= 16; - character += source.readUnsignedShort(); + character += buffer.readUnsignedShort(); } return character; } @@ -728,6 +783,12 @@ public class BinaryDictInputOutput { } flags |= FLAG_HAS_BIGRAMS; } + if (group.mIsNotAWord) { + flags |= FLAG_IS_NOT_A_WORD; + } + if (group.mIsBlacklistEntry) { + flags |= FLAG_IS_BLACKLISTED; + } return flags; } @@ -783,10 +844,10 @@ public class BinaryDictInputOutput { // their lower bound and exclude their higher bound so we need to have the first step // start at exactly 1 unit higher than floor(unigramFreq + half a step). // Note : to reconstruct the score, the dictionary reader will need to divide - // MAX_TERMINAL_FREQUENCY - unigramFreq by 16.5 likewise, and add - // (discretizedFrequency + 0.5) times this value to get the median value of the step, - // which is the best approximation. This is how we get the most precise result with - // only four bits. + // MAX_TERMINAL_FREQUENCY - unigramFreq by 16.5 likewise to get the value of the step, + // and add (discretizedFrequency + 0.5 + 0.5) times this value to get the best + // approximation. (0.5 to get the first step start, and 0.5 to get the middle of the + // step pointed by the discretized frequency. final float stepSize = (MAX_TERMINAL_FREQUENCY - unigramFrequency) / (1.5f + MAX_BIGRAM_FREQUENCY); final float firstStepStart = 1 + unigramFrequency + (stepSize / 2.0f); @@ -1091,46 +1152,46 @@ public class BinaryDictInputOutput { // readDictionaryBinary is the public entry point for them. static final int[] characterBuffer = new int[MAX_WORD_LENGTH]; - private static CharGroupInfo readCharGroup(RandomAccessFile source, - final int originalGroupAddress) throws IOException { + private static CharGroupInfo readCharGroup(final FusionDictionaryBufferInterface buffer, + final int originalGroupAddress) { int addressPointer = originalGroupAddress; - final int flags = source.readUnsignedByte(); + final int flags = buffer.readUnsignedByte(); ++addressPointer; final int characters[]; if (0 != (flags & FLAG_HAS_MULTIPLE_CHARS)) { int index = 0; - int character = CharEncoding.readChar(source); + int character = CharEncoding.readChar(buffer); addressPointer += CharEncoding.getCharSize(character); while (-1 != character) { characterBuffer[index++] = character; - character = CharEncoding.readChar(source); + character = CharEncoding.readChar(buffer); addressPointer += CharEncoding.getCharSize(character); } characters = Arrays.copyOfRange(characterBuffer, 0, index); } else { - final int character = CharEncoding.readChar(source); + final int character = CharEncoding.readChar(buffer); addressPointer += CharEncoding.getCharSize(character); characters = new int[] { character }; } final int frequency; if (0 != (FLAG_IS_TERMINAL & flags)) { ++addressPointer; - frequency = source.readUnsignedByte(); + frequency = buffer.readUnsignedByte(); } else { frequency = CharGroup.NOT_A_TERMINAL; } int childrenAddress = addressPointer; switch (flags & MASK_GROUP_ADDRESS_TYPE) { case FLAG_GROUP_ADDRESS_TYPE_ONEBYTE: - childrenAddress += source.readUnsignedByte(); + childrenAddress += buffer.readUnsignedByte(); addressPointer += 1; break; case FLAG_GROUP_ADDRESS_TYPE_TWOBYTES: - childrenAddress += source.readUnsignedShort(); + childrenAddress += buffer.readUnsignedShort(); addressPointer += 2; break; case FLAG_GROUP_ADDRESS_TYPE_THREEBYTES: - childrenAddress += (source.readUnsignedByte() << 16) + source.readUnsignedShort(); + childrenAddress += buffer.readUnsignedInt24(); addressPointer += 3; break; case FLAG_GROUP_ADDRESS_TYPE_NOADDRESS: @@ -1140,38 +1201,38 @@ public class BinaryDictInputOutput { } ArrayList<WeightedString> shortcutTargets = null; if (0 != (flags & FLAG_HAS_SHORTCUT_TARGETS)) { - final long pointerBefore = source.getFilePointer(); + final int pointerBefore = buffer.position(); shortcutTargets = new ArrayList<WeightedString>(); - source.readUnsignedShort(); // Skip the size + buffer.readUnsignedShort(); // Skip the size while (true) { - final int targetFlags = source.readUnsignedByte(); - final String word = CharEncoding.readString(source); + final int targetFlags = buffer.readUnsignedByte(); + final String word = CharEncoding.readString(buffer); shortcutTargets.add(new WeightedString(word, targetFlags & FLAG_ATTRIBUTE_FREQUENCY)); if (0 == (targetFlags & FLAG_ATTRIBUTE_HAS_NEXT)) break; } - addressPointer += (source.getFilePointer() - pointerBefore); + addressPointer += buffer.position() - pointerBefore; } ArrayList<PendingAttribute> bigrams = null; if (0 != (flags & FLAG_HAS_BIGRAMS)) { bigrams = new ArrayList<PendingAttribute>(); while (true) { - final int bigramFlags = source.readUnsignedByte(); + final int bigramFlags = buffer.readUnsignedByte(); ++addressPointer; final int sign = 0 == (bigramFlags & FLAG_ATTRIBUTE_OFFSET_NEGATIVE) ? 1 : -1; int bigramAddress = addressPointer; switch (bigramFlags & MASK_ATTRIBUTE_ADDRESS_TYPE) { case FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE: - bigramAddress += sign * source.readUnsignedByte(); + bigramAddress += sign * buffer.readUnsignedByte(); addressPointer += 1; break; case FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES: - bigramAddress += sign * source.readUnsignedShort(); + bigramAddress += sign * buffer.readUnsignedShort(); addressPointer += 2; break; case FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES: - final int offset = ((source.readUnsignedByte() << 16) - + source.readUnsignedShort()); + final int offset = (buffer.readUnsignedByte() << 16) + + buffer.readUnsignedShort(); bigramAddress += sign * offset; addressPointer += 3; break; @@ -1188,15 +1249,15 @@ public class BinaryDictInputOutput { } /** - * Reads and returns the char group count out of a file and forwards the pointer. + * Reads and returns the char group count out of a buffer and forwards the pointer. */ - private static int readCharGroupCount(RandomAccessFile source) throws IOException { - final int msb = source.readUnsignedByte(); + private static int readCharGroupCount(final FusionDictionaryBufferInterface buffer) { + final int msb = buffer.readUnsignedByte(); if (MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT >= msb) { return msb; } else { return ((MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT & msb) << 8) - + source.readUnsignedByte(); + + buffer.readUnsignedByte(); } } @@ -1204,31 +1265,29 @@ public class BinaryDictInputOutput { // of this method. Since it performs direct, unbuffered random access to the file and // may be called hundreds of thousands of times, the resulting performance is not // reasonable without some kind of cache. Thus: - // TODO: perform buffered I/O here and in other places in the code. private static TreeMap<Integer, String> wordCache = new TreeMap<Integer, String>(); /** * Finds, as a string, the word at the address passed as an argument. * - * @param source the file to read from. + * @param buffer the buffer to read from. * @param headerSize the size of the header. * @param address the address to seek. * @return the word, as a string. - * @throws IOException if the file can't be read. */ - private static String getWordAtAddress(final RandomAccessFile source, final long headerSize, - int address) throws IOException { + private static String getWordAtAddress(final FusionDictionaryBufferInterface buffer, + final int headerSize, final int address) { final String cachedString = wordCache.get(address); if (null != cachedString) return cachedString; - final long originalPointer = source.getFilePointer(); - source.seek(headerSize); - final int count = readCharGroupCount(source); + final int originalPointer = buffer.position(); + buffer.position(headerSize); + final int count = readCharGroupCount(buffer); int groupOffset = getGroupCountSize(count); final StringBuilder builder = new StringBuilder(); String result = null; CharGroupInfo last = null; for (int i = count - 1; i >= 0; --i) { - CharGroupInfo info = readCharGroup(source, groupOffset); + CharGroupInfo info = readCharGroup(buffer, groupOffset); groupOffset = info.mEndAddress; if (info.mOriginalAddress == address) { builder.append(new String(info.mCharacters, 0, info.mCharacters.length)); @@ -1239,9 +1298,9 @@ public class BinaryDictInputOutput { if (info.mChildrenAddress > address) { if (null == last) continue; builder.append(new String(last.mCharacters, 0, last.mCharacters.length)); - source.seek(last.mChildrenAddress + headerSize); + buffer.position(last.mChildrenAddress + headerSize); groupOffset = last.mChildrenAddress + 1; - i = source.readUnsignedByte(); + i = buffer.readUnsignedByte(); last = null; continue; } @@ -1249,64 +1308,69 @@ public class BinaryDictInputOutput { } if (0 == i && hasChildrenAddress(last.mChildrenAddress)) { builder.append(new String(last.mCharacters, 0, last.mCharacters.length)); - source.seek(last.mChildrenAddress + headerSize); + buffer.position(last.mChildrenAddress + headerSize); groupOffset = last.mChildrenAddress + 1; - i = source.readUnsignedByte(); + i = buffer.readUnsignedByte(); last = null; continue; } } - source.seek(originalPointer); + buffer.position(originalPointer); wordCache.put(address, result); return result; } /** - * Reads a single node from a binary file. + * Reads a single node from a buffer. * - * This methods reads the file at the current position of its file pointer. A node is - * fully expected to start at the current position. + * This methods reads the file at the current position. A node is fully expected to start at + * the current position. * This will recursively read other nodes into the structure, populating the reverse * maps on the fly and using them to keep track of already read nodes. * - * @param source the data file, correctly positioned at the start of a node. + * @param buffer the buffer, correctly positioned at the start of a node. * @param headerSize the size, in bytes, of the file header. * @param reverseNodeMap a mapping from addresses to already read nodes. * @param reverseGroupMap a mapping from addresses to already read character groups. * @return the read node with all his children already read. */ - private static Node readNode(RandomAccessFile source, long headerSize, - Map<Integer, Node> reverseNodeMap, Map<Integer, CharGroup> reverseGroupMap) + private static Node readNode(final FusionDictionaryBufferInterface buffer, final int headerSize, + final Map<Integer, Node> reverseNodeMap, final Map<Integer, CharGroup> reverseGroupMap) throws IOException { - final int nodeOrigin = (int)(source.getFilePointer() - headerSize); - final int count = readCharGroupCount(source); + final int nodeOrigin = buffer.position() - headerSize; + final int count = readCharGroupCount(buffer); final ArrayList<CharGroup> nodeContents = new ArrayList<CharGroup>(); int groupOffset = nodeOrigin + getGroupCountSize(count); for (int i = count; i > 0; --i) { - CharGroupInfo info = readCharGroup(source, groupOffset); + CharGroupInfo info = readCharGroup(buffer, groupOffset); ArrayList<WeightedString> shortcutTargets = info.mShortcutTargets; ArrayList<WeightedString> bigrams = null; if (null != info.mBigrams) { bigrams = new ArrayList<WeightedString>(); for (PendingAttribute bigram : info.mBigrams) { - final String word = getWordAtAddress(source, headerSize, bigram.mAddress); + final String word = getWordAtAddress( + buffer, headerSize, bigram.mAddress); bigrams.add(new WeightedString(word, bigram.mFrequency)); } } if (hasChildrenAddress(info.mChildrenAddress)) { Node children = reverseNodeMap.get(info.mChildrenAddress); if (null == children) { - final long currentPosition = source.getFilePointer(); - source.seek(info.mChildrenAddress + headerSize); - children = readNode(source, headerSize, reverseNodeMap, reverseGroupMap); - source.seek(currentPosition); + final int currentPosition = buffer.position(); + buffer.position(info.mChildrenAddress + headerSize); + children = readNode( + buffer, headerSize, reverseNodeMap, reverseGroupMap); + buffer.position(currentPosition); } nodeContents.add( new CharGroup(info.mCharacters, shortcutTargets, bigrams, info.mFrequency, - children)); + 0 != (info.mFlags & FLAG_IS_NOT_A_WORD), + 0 != (info.mFlags & FLAG_IS_BLACKLISTED), children)); } else { nodeContents.add( - new CharGroup(info.mCharacters, shortcutTargets, bigrams, info.mFrequency)); + new CharGroup(info.mCharacters, shortcutTargets, bigrams, info.mFrequency, + 0 != (info.mFlags & FLAG_IS_NOT_A_WORD), + 0 != (info.mFlags & FLAG_IS_BLACKLISTED))); } groupOffset = info.mEndAddress; } @@ -1316,59 +1380,205 @@ public class BinaryDictInputOutput { return node; } + // TODO: move these methods (readUnigramsAndBigramsBinary(|Inner)) and an inner class (Position) + // out of this class. + private static class Position { + public static final int NOT_READ_GROUPCOUNT = -1; + + public int mAddress; + public int mNumOfCharGroup; + public int mPosition; + public int mLength; + + public Position(int address, int length) { + mAddress = address; + mLength = length; + mNumOfCharGroup = NOT_READ_GROUPCOUNT; + } + } + + /** + * Tours all node without recursive call. + */ + private static void readUnigramsAndBigramsBinaryInner( + final FusionDictionaryBufferInterface buffer, final int headerSize, + final Map<Integer, String> words, final Map<Integer, Integer> frequencies, + final Map<Integer, ArrayList<PendingAttribute>> bigrams) { + int[] pushedChars = new int[MAX_WORD_LENGTH + 1]; + + Stack<Position> stack = new Stack<Position>(); + int index = 0; + + Position initPos = new Position(headerSize, 0); + stack.push(initPos); + + while (!stack.empty()) { + Position p = stack.peek(); + + if (DBG) { + MakedictLog.d("read: address=" + p.mAddress + ", numOfCharGroup=" + + p.mNumOfCharGroup + ", position=" + p.mPosition + ", length=" + p.mLength); + } + + if (buffer.position() != p.mAddress) buffer.position(p.mAddress); + if (index != p.mLength) index = p.mLength; + + if (p.mNumOfCharGroup == Position.NOT_READ_GROUPCOUNT) { + p.mNumOfCharGroup = readCharGroupCount(buffer); + p.mAddress += getGroupCountSize(p.mNumOfCharGroup); + p.mPosition = 0; + } + + CharGroupInfo info = readCharGroup(buffer, p.mAddress - headerSize); + for (int i = 0; i < info.mCharacters.length; ++i) { + pushedChars[index++] = info.mCharacters[i]; + } + p.mPosition++; + + if (info.mFrequency != FusionDictionary.CharGroup.NOT_A_TERMINAL) { // found word + words.put(info.mOriginalAddress, new String(pushedChars, 0, index)); + frequencies.put(info.mOriginalAddress, info.mFrequency); + if (info.mBigrams != null) bigrams.put(info.mOriginalAddress, info.mBigrams); + } + + if (p.mPosition == p.mNumOfCharGroup) { + stack.pop(); + } else { + // the node has more groups. + p.mAddress = buffer.position(); + } + + if (hasChildrenAddress(info.mChildrenAddress)) { + Position childrenPos = new Position(info.mChildrenAddress + headerSize, index); + stack.push(childrenPos); + } + } + } + + /** + * Reads unigrams and bigrams from the binary file. + * Doesn't make the memory representation of the dictionary. + * + * @param buffer the buffer to read. + * @param words the map to store the address as a key and the word as a value. + * @param frequencies the map to store the address as a key and the frequency as a value. + * @param bigrams the map to store the address as a key and the list of address as a value. + * @throws IOException + * @throws UnsupportedFormatException + */ + public static void readUnigramsAndBigramsBinary(final FusionDictionaryBufferInterface buffer, + final Map<Integer, String> words, final Map<Integer, Integer> frequencies, + final Map<Integer, ArrayList<PendingAttribute>> bigrams) throws IOException, + UnsupportedFormatException { + // Read header + final int version = checkFormatVersion(buffer); + final int optionsFlags = buffer.readUnsignedShort(); + final HashMap<String, String> options = new HashMap<String, String>(); + final int headerSize = readHeader(buffer, options, version); + + readUnigramsAndBigramsBinaryInner(buffer, headerSize, words, frequencies, bigrams); + } + /** * Helper function to get the binary format version from the header. + * @throws IOException */ - private static int getFormatVersion(final RandomAccessFile source) throws IOException { - final int magic_v1 = source.readUnsignedShort(); - if (VERSION_1_MAGIC_NUMBER == magic_v1) return source.readUnsignedByte(); - final int magic_v2 = (magic_v1 << 16) + source.readUnsignedShort(); - if (VERSION_2_MAGIC_NUMBER == magic_v2) return source.readUnsignedShort(); + private static int getFormatVersion(final FusionDictionaryBufferInterface buffer) + throws IOException { + final int magic_v1 = buffer.readUnsignedShort(); + if (VERSION_1_MAGIC_NUMBER == magic_v1) return buffer.readUnsignedByte(); + final int magic_v2 = (magic_v1 << 16) + buffer.readUnsignedShort(); + if (VERSION_2_MAGIC_NUMBER == magic_v2) return buffer.readUnsignedShort(); return NOT_A_VERSION_NUMBER; } /** - * Reads a random access file and returns the memory representation of the dictionary. - * - * This high-level method takes a binary file and reads its contents, populating a - * FusionDictionary structure. The optional dict argument is an existing dictionary to - * which words from the file should be added. If it is null, a new dictionary is created. - * - * @param source the file to read. - * @param dict an optional dictionary to add words to, or null. - * @return the created (or merged) dictionary. + * Helper function to get and validate the binary format version. + * @throws UnsupportedFormatException + * @throws IOException */ - public static FusionDictionary readDictionaryBinary(final RandomAccessFile source, - final FusionDictionary dict) throws IOException, UnsupportedFormatException { - // Check file version - final int version = getFormatVersion(source); - if (version < MINIMUM_SUPPORTED_VERSION || version > MAXIMUM_SUPPORTED_VERSION ) { + private static int checkFormatVersion(final FusionDictionaryBufferInterface buffer) + throws IOException, UnsupportedFormatException { + final int version = getFormatVersion(buffer); + if (version < MINIMUM_SUPPORTED_VERSION || version > MAXIMUM_SUPPORTED_VERSION) { throw new UnsupportedFormatException("This file has version " + version + ", but this implementation does not support versions above " + MAXIMUM_SUPPORTED_VERSION); } + return version; + } - // Read options - final int optionsFlags = source.readUnsignedShort(); - - final long headerSize; - final HashMap<String, String> options = new HashMap<String, String>(); + /** + * Reads a header from a buffer. + * @throws IOException + * @throws UnsupportedFormatException + */ + private static int readHeader(final FusionDictionaryBufferInterface buffer, + final HashMap<String, String> options, final int version) + throws IOException, UnsupportedFormatException { + final int headerSize; if (version < FIRST_VERSION_WITH_HEADER_SIZE) { - headerSize = source.getFilePointer(); + headerSize = buffer.position(); } else { - headerSize = (source.readUnsignedByte() << 24) + (source.readUnsignedByte() << 16) - + (source.readUnsignedByte() << 8) + source.readUnsignedByte(); - while (source.getFilePointer() < headerSize) { - final String key = CharEncoding.readString(source); - final String value = CharEncoding.readString(source); - options.put(key, value); - } - source.seek(headerSize); + headerSize = buffer.readInt(); + populateOptions(buffer, headerSize, options); + buffer.position(headerSize); } + if (headerSize < 0) { + throw new UnsupportedFormatException("header size can't be negative."); + } + return headerSize; + } + + /** + * Reads options from a buffer and populate a map with their contents. + * + * The buffer is read at the current position, so the caller must take care the pointer + * is in the right place before calling this. + */ + public static void populateOptions(final FusionDictionaryBufferInterface buffer, + final int headerSize, final HashMap<String, String> options) { + while (buffer.position() < headerSize) { + final String key = CharEncoding.readString(buffer); + final String value = CharEncoding.readString(buffer); + options.put(key, value); + } + } + // TODO: remove this method. + public static void populateOptions(final ByteBuffer buffer, final int headerSize, + final HashMap<String, String> options) { + populateOptions(new ByteBufferWrapper(buffer), headerSize, options); + } + + /** + * Reads a buffer and returns the memory representation of the dictionary. + * + * This high-level method takes a buffer and reads its contents, populating a + * FusionDictionary structure. The optional dict argument is an existing dictionary to + * which words from the buffer should be added. If it is null, a new dictionary is created. + * + * @param buffer the buffer to read. + * @param dict an optional dictionary to add words to, or null. + * @return the created (or merged) dictionary. + */ + public static FusionDictionary readDictionaryBinary( + final FusionDictionaryBufferInterface buffer, final FusionDictionary dict) + throws IOException, UnsupportedFormatException { + // clear cache + wordCache.clear(); + + // Read header + final int version = checkFormatVersion(buffer); + final int optionsFlags = buffer.readUnsignedShort(); + + final HashMap<String, String> options = new HashMap<String, String>(); + final int headerSize = readHeader(buffer, options, version); + Map<Integer, Node> reverseNodeMapping = new TreeMap<Integer, Node>(); Map<Integer, CharGroup> reverseGroupMapping = new TreeMap<Integer, CharGroup>(); - final Node root = readNode(source, headerSize, reverseNodeMapping, reverseGroupMapping); + final Node root = readNode( + buffer, headerSize, reverseNodeMapping, reverseGroupMapping); FusionDictionary newDict = new FusionDictionary(root, new FusionDictionary.DictionaryOptions(options, @@ -1376,7 +1586,11 @@ public class BinaryDictInputOutput { 0 != (optionsFlags & FRENCH_LIGATURE_PROCESSING_FLAG))); if (null != dict) { for (final Word w : dict) { - newDict.add(w.mWord, w.mFrequency, w.mShortcutTargets); + if (w.mIsBlacklistEntry) { + newDict.addBlacklistEntry(w.mWord, w.mShortcutTargets, w.mIsNotAWord); + } else { + newDict.add(w.mWord, w.mFrequency, w.mShortcutTargets, w.mIsNotAWord); + } } for (final Word w : dict) { // By construction a binary dictionary may not have bigrams pointing to @@ -1391,6 +1605,12 @@ public class BinaryDictInputOutput { return newDict; } + // TODO: remove this method. + public static FusionDictionary readDictionaryBinary(final ByteBuffer buffer, + final FusionDictionary dict) throws IOException, UnsupportedFormatException { + return readDictionaryBinary(new ByteBufferWrapper(buffer), dict); + } + /** * Basic test to find out whether the file is a binary dictionary or not. * @@ -1400,14 +1620,44 @@ public class BinaryDictInputOutput { * @return true if it's a binary dictionary, false otherwise */ public static boolean isBinaryDictionary(final String filename) { + FileInputStream inStream = null; try { - RandomAccessFile f = new RandomAccessFile(filename, "r"); - final int version = getFormatVersion(f); + final File file = new File(filename); + inStream = new FileInputStream(file); + final ByteBuffer buffer = inStream.getChannel().map( + FileChannel.MapMode.READ_ONLY, 0, file.length()); + final int version = getFormatVersion(new ByteBufferWrapper(buffer)); return (version >= MINIMUM_SUPPORTED_VERSION && version <= MAXIMUM_SUPPORTED_VERSION); } catch (FileNotFoundException e) { return false; } catch (IOException e) { return false; + } finally { + if (inStream != null) { + try { + inStream.close(); + } catch (IOException e) { + // do nothing + } + } } } + + /** + * Calculate bigram frequency from compressed value + * + * @see #makeBigramFlags + * + * @param unigramFrequency + * @param bigramFrequency compressed frequency + * @return approximate bigram frequency + */ + public static int reconstructBigramFrequency(final int unigramFrequency, + final int bigramFrequency) { + final float stepSize = (MAX_TERMINAL_FREQUENCY - unigramFrequency) + / (1.5f + MAX_BIGRAM_FREQUENCY); + final float resultFreqFloat = (float)unigramFrequency + + stepSize * (bigramFrequency + 1.0f); + return (int)resultFreqFloat; + } } diff --git a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java index 5864db28e..f1abea9ec 100644 --- a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java +++ b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java @@ -101,26 +101,34 @@ public class FusionDictionary implements Iterable<Word> { ArrayList<WeightedString> mBigrams; int mFrequency; // NOT_A_TERMINAL == mFrequency indicates this is not a terminal. Node mChildren; + boolean mIsNotAWord; // Only a shortcut + boolean mIsBlacklistEntry; // The two following members to help with binary generation int mCachedSize; int mCachedAddress; public CharGroup(final int[] chars, final ArrayList<WeightedString> shortcutTargets, - final ArrayList<WeightedString> bigrams, final int frequency) { + final ArrayList<WeightedString> bigrams, final int frequency, + final boolean isNotAWord, final boolean isBlacklistEntry) { mChars = chars; mFrequency = frequency; mShortcutTargets = shortcutTargets; mBigrams = bigrams; mChildren = null; + mIsNotAWord = isNotAWord; + mIsBlacklistEntry = isBlacklistEntry; } public CharGroup(final int[] chars, final ArrayList<WeightedString> shortcutTargets, - final ArrayList<WeightedString> bigrams, final int frequency, final Node children) { + final ArrayList<WeightedString> bigrams, final int frequency, + final boolean isNotAWord, final boolean isBlacklistEntry, final Node children) { mChars = chars; mFrequency = frequency; mShortcutTargets = shortcutTargets; mBigrams = bigrams; mChildren = children; + mIsNotAWord = isNotAWord; + mIsBlacklistEntry = isBlacklistEntry; } public void addChild(CharGroup n) { @@ -197,8 +205,9 @@ public class FusionDictionary implements Iterable<Word> { * the existing ones if any. Note: unigram, bigram, and shortcut frequencies are only * updated if they are higher than the existing ones. */ - public void update(int frequency, ArrayList<WeightedString> shortcutTargets, - ArrayList<WeightedString> bigrams) { + public void update(final int frequency, final ArrayList<WeightedString> shortcutTargets, + final ArrayList<WeightedString> bigrams, + final boolean isNotAWord, final boolean isBlacklistEntry) { if (frequency > mFrequency) { mFrequency = frequency; } @@ -234,6 +243,8 @@ public class FusionDictionary implements Iterable<Word> { } } } + mIsNotAWord = isNotAWord; + mIsBlacklistEntry = isBlacklistEntry; } } @@ -296,10 +307,24 @@ public class FusionDictionary implements Iterable<Word> { * @param word the word to add. * @param frequency the frequency of the word, in the range [0..255]. * @param shortcutTargets a list of shortcut targets for this word, or null. + * @param isNotAWord true if this should not be considered a word (e.g. shortcut only) */ public void add(final String word, final int frequency, - final ArrayList<WeightedString> shortcutTargets) { - add(getCodePoints(word), frequency, shortcutTargets); + final ArrayList<WeightedString> shortcutTargets, final boolean isNotAWord) { + add(getCodePoints(word), frequency, shortcutTargets, isNotAWord, + false /* isBlacklistEntry */); + } + + /** + * Helper method to add a blacklist entry as a string. + * + * @param word the word to add as a blacklist entry. + * @param shortcutTargets a list of shortcut targets for this word, or null. + * @param isNotAWord true if this is not a word for spellcheking purposes (shortcut only or so) + */ + public void addBlacklistEntry(final String word, + final ArrayList<WeightedString> shortcutTargets, final boolean isNotAWord) { + add(getCodePoints(word), 0, shortcutTargets, isNotAWord, true /* isBlacklistEntry */); } /** @@ -332,7 +357,8 @@ public class FusionDictionary implements Iterable<Word> { if (charGroup != null) { final CharGroup charGroup2 = findWordInTree(mRoot, word2); if (charGroup2 == null) { - add(getCodePoints(word2), 0, null); + add(getCodePoints(word2), 0, null, false /* isNotAWord */, + false /* isBlacklistEntry */); } charGroup.addBigram(word2, frequency); } else { @@ -349,9 +375,12 @@ public class FusionDictionary implements Iterable<Word> { * @param word the word, as an int array. * @param frequency the frequency of the word, in the range [0..255]. * @param shortcutTargets an optional list of shortcut targets for this word (null if none). + * @param isNotAWord true if this is not a word for spellcheking purposes (shortcut only or so) + * @param isBlacklistEntry true if this is a blacklisted word, false otherwise */ private void add(final int[] word, final int frequency, - final ArrayList<WeightedString> shortcutTargets) { + final ArrayList<WeightedString> shortcutTargets, + final boolean isNotAWord, final boolean isBlacklistEntry) { assert(frequency >= 0 && frequency <= 255); Node currentNode = mRoot; int charIndex = 0; @@ -376,7 +405,7 @@ public class FusionDictionary implements Iterable<Word> { final int insertionIndex = findInsertionIndex(currentNode, word[charIndex]); final CharGroup newGroup = new CharGroup( Arrays.copyOfRange(word, charIndex, word.length), - shortcutTargets, null /* bigrams */, frequency); + shortcutTargets, null /* bigrams */, frequency, isNotAWord, isBlacklistEntry); currentNode.mData.add(insertionIndex, newGroup); if (DBG) checkStack(currentNode); } else { @@ -386,13 +415,15 @@ public class FusionDictionary implements Iterable<Word> { // The new word is a prefix of an existing word, but the node on which it // should end already exists as is. Since the old CharNode was not a terminal, // make it one by filling in its frequency and other attributes - currentGroup.update(frequency, shortcutTargets, null); + currentGroup.update(frequency, shortcutTargets, null, isNotAWord, + isBlacklistEntry); } else { // The new word matches the full old word and extends past it. // We only have to create a new node and add it to the end of this. final CharGroup newNode = new CharGroup( Arrays.copyOfRange(word, charIndex + differentCharIndex, word.length), - shortcutTargets, null /* bigrams */, frequency); + shortcutTargets, null /* bigrams */, frequency, isNotAWord, + isBlacklistEntry); currentGroup.mChildren = new Node(); currentGroup.mChildren.mData.add(newNode); } @@ -400,7 +431,9 @@ public class FusionDictionary implements Iterable<Word> { if (0 == differentCharIndex) { // Exact same word. Update the frequency if higher. This will also add the // new shortcuts to the existing shortcut list if it already exists. - currentGroup.update(frequency, shortcutTargets, null); + currentGroup.update(frequency, shortcutTargets, null, + currentGroup.mIsNotAWord && isNotAWord, + currentGroup.mIsBlacklistEntry || isBlacklistEntry); } else { // Partial prefix match only. We have to replace the current node with a node // containing the current prefix and create two new ones for the tails. @@ -408,21 +441,26 @@ public class FusionDictionary implements Iterable<Word> { final CharGroup newOldWord = new CharGroup( Arrays.copyOfRange(currentGroup.mChars, differentCharIndex, currentGroup.mChars.length), currentGroup.mShortcutTargets, - currentGroup.mBigrams, currentGroup.mFrequency, currentGroup.mChildren); + currentGroup.mBigrams, currentGroup.mFrequency, + currentGroup.mIsNotAWord, currentGroup.mIsBlacklistEntry, + currentGroup.mChildren); newChildren.mData.add(newOldWord); final CharGroup newParent; if (charIndex + differentCharIndex >= word.length) { newParent = new CharGroup( Arrays.copyOfRange(currentGroup.mChars, 0, differentCharIndex), - shortcutTargets, null /* bigrams */, frequency, newChildren); + shortcutTargets, null /* bigrams */, frequency, + isNotAWord, isBlacklistEntry, newChildren); } else { newParent = new CharGroup( Arrays.copyOfRange(currentGroup.mChars, 0, differentCharIndex), - null /* shortcutTargets */, null /* bigrams */, -1, newChildren); + null /* shortcutTargets */, null /* bigrams */, -1, + false /* isNotAWord */, false /* isBlacklistEntry */, newChildren); final CharGroup newWord = new CharGroup(Arrays.copyOfRange(word, charIndex + differentCharIndex, word.length), - shortcutTargets, null /* bigrams */, frequency); + shortcutTargets, null /* bigrams */, frequency, + isNotAWord, isBlacklistEntry); final int addIndex = word[charIndex + differentCharIndex] > currentGroup.mChars[differentCharIndex] ? 1 : 0; newChildren.mData.add(addIndex, newWord); @@ -483,7 +521,8 @@ public class FusionDictionary implements Iterable<Word> { private static int findInsertionIndex(final Node node, int character) { final ArrayList<CharGroup> data = node.mData; final CharGroup reference = new CharGroup(new int[] { character }, - null /* shortcutTargets */, null /* bigrams */, 0); + null /* shortcutTargets */, null /* bigrams */, 0, false /* isNotAWord */, + false /* isBlacklistEntry */); int result = Collections.binarySearch(data, reference, CHARGROUP_COMPARATOR); return result >= 0 ? result : -result - 1; } @@ -516,13 +555,23 @@ public class FusionDictionary implements Iterable<Word> { int indexOfGroup = findIndexOfChar(node, s.codePointAt(index)); if (CHARACTER_NOT_FOUND == indexOfGroup) return null; currentGroup = node.mData.get(indexOfGroup); + + if (s.length() - index < currentGroup.mChars.length) return null; + int newIndex = index; + while (newIndex < s.length() && newIndex - index < currentGroup.mChars.length) { + if (currentGroup.mChars[newIndex - index] != s.codePointAt(newIndex)) return null; + newIndex++; + } + index = newIndex; + if (DBG) checker.append(new String(currentGroup.mChars, 0, currentGroup.mChars.length)); - index += currentGroup.mChars.length; if (index < s.length()) { node = currentGroup.mChildren; } } while (null != node && index < s.length()); + if (index < s.length()) return null; + if (!currentGroup.isTerminal()) return null; if (DBG && !s.equals(checker.toString())) return null; return currentGroup; } @@ -738,7 +787,8 @@ public class FusionDictionary implements Iterable<Word> { } if (currentGroup.mFrequency >= 0) return new Word(mCurrentString.toString(), currentGroup.mFrequency, - currentGroup.mShortcutTargets, currentGroup.mBigrams); + currentGroup.mShortcutTargets, currentGroup.mBigrams, + currentGroup.mIsNotAWord, currentGroup.mIsBlacklistEntry); } else { mPositions.removeLast(); currentPos = mPositions.getLast(); diff --git a/java/src/com/android/inputmethod/latin/makedict/Word.java b/java/src/com/android/inputmethod/latin/makedict/Word.java index 65fc72c40..4683ef154 100644 --- a/java/src/com/android/inputmethod/latin/makedict/Word.java +++ b/java/src/com/android/inputmethod/latin/makedict/Word.java @@ -31,16 +31,21 @@ public class Word implements Comparable<Word> { public final int mFrequency; public final ArrayList<WeightedString> mShortcutTargets; public final ArrayList<WeightedString> mBigrams; + public final boolean mIsNotAWord; + public final boolean mIsBlacklistEntry; private int mHashCode = 0; public Word(final String word, final int frequency, final ArrayList<WeightedString> shortcutTargets, - final ArrayList<WeightedString> bigrams) { + final ArrayList<WeightedString> bigrams, + final boolean isNotAWord, final boolean isBlacklistEntry) { mWord = word; mFrequency = frequency; mShortcutTargets = shortcutTargets; mBigrams = bigrams; + mIsNotAWord = isNotAWord; + mIsBlacklistEntry = isBlacklistEntry; } private static int computeHashCode(Word word) { @@ -48,7 +53,9 @@ public class Word implements Comparable<Word> { word.mWord, word.mFrequency, word.mShortcutTargets.hashCode(), - word.mBigrams.hashCode() + word.mBigrams.hashCode(), + word.mIsNotAWord, + word.mIsBlacklistEntry }); } @@ -78,7 +85,9 @@ public class Word implements Comparable<Word> { Word w = (Word)o; return mFrequency == w.mFrequency && mWord.equals(w.mWord) && mShortcutTargets.equals(w.mShortcutTargets) - && mBigrams.equals(w.mBigrams); + && mBigrams.equals(w.mBigrams) + && mIsNotAWord == w.mIsNotAWord + && mIsBlacklistEntry == w.mIsBlacklistEntry; } @Override diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java index 3bdfe1f27..eef7a51f2 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java @@ -25,6 +25,7 @@ import android.view.textservice.SuggestionsInfo; import com.android.inputmethod.keyboard.ProximityInfo; import com.android.inputmethod.latin.BinaryDictionary; +import com.android.inputmethod.latin.CollectionUtils; import com.android.inputmethod.latin.ContactsBinaryDictionary; import com.android.inputmethod.latin.Dictionary; import com.android.inputmethod.latin.DictionaryCollection; @@ -35,7 +36,6 @@ import com.android.inputmethod.latin.StringUtils; import com.android.inputmethod.latin.SynchronouslyLoadedContactsBinaryDictionary; import com.android.inputmethod.latin.SynchronouslyLoadedUserBinaryDictionary; import com.android.inputmethod.latin.UserBinaryDictionary; -import com.android.inputmethod.latin.WhitelistDictionary; import java.lang.ref.WeakReference; import java.util.ArrayList; @@ -63,12 +63,9 @@ public class AndroidSpellCheckerService extends SpellCheckerService public static final int CAPITALIZE_ALL = 2; // All caps private final static String[] EMPTY_STRING_ARRAY = new String[0]; - private Map<String, DictionaryPool> mDictionaryPools = - Collections.synchronizedMap(new TreeMap<String, DictionaryPool>()); + private Map<String, DictionaryPool> mDictionaryPools = CollectionUtils.newSynchronizedTreeMap(); private Map<String, UserBinaryDictionary> mUserDictionaries = - Collections.synchronizedMap(new TreeMap<String, UserBinaryDictionary>()); - private Map<String, Dictionary> mWhitelistDictionaries = - Collections.synchronizedMap(new TreeMap<String, Dictionary>()); + CollectionUtils.newSynchronizedTreeMap(); private ContactsBinaryDictionary mContactsDictionary; // The threshold for a candidate to be offered as a suggestion. @@ -80,7 +77,7 @@ public class AndroidSpellCheckerService extends SpellCheckerService private final Object mUseContactsLock = new Object(); private final HashSet<WeakReference<DictionaryCollection>> mDictionaryCollectionsList = - new HashSet<WeakReference<DictionaryCollection>>(); + CollectionUtils.newHashSet(); public static final int SCRIPT_LATIN = 0; public static final int SCRIPT_CYRILLIC = 1; @@ -96,7 +93,7 @@ public class AndroidSpellCheckerService extends SpellCheckerService // proximity to pass to the dictionary descent algorithm. // IMPORTANT: this only contains languages - do not write countries in there. // Only the language is searched from the map. - mLanguageToScript = new TreeMap<String, Integer>(); + mLanguageToScript = CollectionUtils.newTreeMap(); mLanguageToScript.put("en", SCRIPT_LATIN); mLanguageToScript.put("fr", SCRIPT_LATIN); mLanguageToScript.put("de", SCRIPT_LATIN); @@ -234,7 +231,7 @@ public class AndroidSpellCheckerService extends SpellCheckerService mSuggestionThreshold = suggestionThreshold; mRecommendedThreshold = recommendedThreshold; mMaxLength = maxLength; - mSuggestions = new ArrayList<CharSequence>(maxLength + 1); + mSuggestions = CollectionUtils.newArrayList(maxLength + 1); mScores = new int[mMaxLength]; } @@ -362,12 +359,9 @@ public class AndroidSpellCheckerService extends SpellCheckerService private void closeAllDictionaries() { final Map<String, DictionaryPool> oldPools = mDictionaryPools; - mDictionaryPools = Collections.synchronizedMap(new TreeMap<String, DictionaryPool>()); + mDictionaryPools = CollectionUtils.newSynchronizedTreeMap(); final Map<String, UserBinaryDictionary> oldUserDictionaries = mUserDictionaries; - mUserDictionaries = - Collections.synchronizedMap(new TreeMap<String, UserBinaryDictionary>()); - final Map<String, Dictionary> oldWhitelistDictionaries = mWhitelistDictionaries; - mWhitelistDictionaries = Collections.synchronizedMap(new TreeMap<String, Dictionary>()); + mUserDictionaries = CollectionUtils.newSynchronizedTreeMap(); new Thread("spellchecker_close_dicts") { @Override public void run() { @@ -377,9 +371,6 @@ public class AndroidSpellCheckerService extends SpellCheckerService for (Dictionary dict : oldUserDictionaries.values()) { dict.close(); } - for (Dictionary dict : oldWhitelistDictionaries.values()) { - dict.close(); - } synchronized (mUseContactsLock) { if (null != mContactsDictionary) { // The synchronously loaded contacts dictionary should have been in one @@ -423,12 +414,6 @@ public class AndroidSpellCheckerService extends SpellCheckerService mUserDictionaries.put(localeStr, userDictionary); } dictionaryCollection.addDictionary(userDictionary); - Dictionary whitelistDictionary = mWhitelistDictionaries.get(localeStr); - if (null == whitelistDictionary) { - whitelistDictionary = new WhitelistDictionary(this, locale); - mWhitelistDictionaries.put(localeStr, whitelistDictionary); - } - dictionaryCollection.addDictionary(whitelistDictionary); synchronized (mUseContactsLock) { if (mUseContactsDictionary) { if (null == mContactsDictionary) { diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java index 501a0e221..5a1bd37f5 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java @@ -22,6 +22,8 @@ import android.view.textservice.SentenceSuggestionsInfo; import android.view.textservice.SuggestionsInfo; import android.view.textservice.TextInfo; +import com.android.inputmethod.latin.CollectionUtils; + import java.util.ArrayList; public class AndroidSpellCheckerSession extends AndroidWordLevelSpellCheckerSession { @@ -40,10 +42,10 @@ public class AndroidSpellCheckerSession extends AndroidWordLevelSpellCheckerSess return null; } final int N = ssi.getSuggestionsCount(); - final ArrayList<Integer> additionalOffsets = new ArrayList<Integer>(); - final ArrayList<Integer> additionalLengths = new ArrayList<Integer>(); + final ArrayList<Integer> additionalOffsets = CollectionUtils.newArrayList(); + final ArrayList<Integer> additionalLengths = CollectionUtils.newArrayList(); final ArrayList<SuggestionsInfo> additionalSuggestionsInfos = - new ArrayList<SuggestionsInfo>(); + CollectionUtils.newArrayList(); String currentWord = null; for (int i = 0; i < N; ++i) { final SuggestionsInfo si = ssi.getSuggestionsInfoAt(i); diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java index 06f5db749..f4784ff1a 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java @@ -24,6 +24,7 @@ import android.view.textservice.SuggestionsInfo; import android.view.textservice.TextInfo; import com.android.inputmethod.compat.SuggestionsInfoCompatUtils; +import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.LocaleUtils; import com.android.inputmethod.latin.WordComposer; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; @@ -194,7 +195,7 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { DictAndProximity dictInfo = null; try { dictInfo = mDictionaryPool.pollWithDefaultTimeout(); - if (null == dictInfo) { + if (!DictionaryPool.isAValidDictionary(dictInfo)) { return AndroidSpellCheckerService.getNotInDictEmptySuggestions(); } return dictInfo.mDictionary.isValidWord(inText) @@ -225,8 +226,8 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { final int xy = SpellCheckerProximityInfo.getXYForCodePointAndScript( codePoint, mScript); if (SpellCheckerProximityInfo.NOT_A_COORDINATE_PAIR == xy) { - composer.add(codePoint, WordComposer.NOT_A_COORDINATE, - WordComposer.NOT_A_COORDINATE); + composer.add(codePoint, + Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); } else { composer.add(codePoint, xy & 0xFFFF, xy >> 16); } @@ -237,7 +238,7 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { DictAndProximity dictInfo = null; try { dictInfo = mDictionaryPool.pollWithDefaultTimeout(); - if (null == dictInfo) { + if (!DictionaryPool.isAValidDictionary(dictInfo)) { return AndroidSpellCheckerService.getNotInDictEmptySuggestions(); } final ArrayList<SuggestedWordInfo> suggestions = diff --git a/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java b/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java index 83f82faeb..53aa6c719 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java @@ -18,6 +18,13 @@ package com.android.inputmethod.latin.spellcheck; import android.util.Log; +import com.android.inputmethod.keyboard.ProximityInfo; +import com.android.inputmethod.latin.CollectionUtils; +import com.android.inputmethod.latin.Dictionary; +import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; +import com.android.inputmethod.latin.WordComposer; + +import java.util.ArrayList; import java.util.Locale; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; @@ -39,6 +46,26 @@ public class DictionaryPool extends LinkedBlockingQueue<DictAndProximity> { private final Locale mLocale; private int mSize; private volatile boolean mClosed; + final static ArrayList<SuggestedWordInfo> noSuggestions = CollectionUtils.newArrayList(); + private final static DictAndProximity dummyDict = new DictAndProximity( + new Dictionary(Dictionary.TYPE_MAIN) { + @Override + public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer, + final CharSequence prevWord, final ProximityInfo proximityInfo) { + return noSuggestions; + } + @Override + public boolean isValidWord(CharSequence word) { + // This is never called. However if for some strange reason it ever gets + // called, returning true is less destructive (it will not underline the + // word in red). + return true; + } + }, null); + + static public boolean isAValidDictionary(final DictAndProximity dictInfo) { + return null != dictInfo && dummyDict != dictInfo; + } public DictionaryPool(final int maxSize, final AndroidSpellCheckerService service, final Locale locale) { @@ -98,7 +125,7 @@ public class DictionaryPool extends LinkedBlockingQueue<DictAndProximity> { public boolean offer(final DictAndProximity dict) { if (mClosed) { dict.mDictionary.close(); - return false; + return super.offer(dummyDict); } else { return super.offer(dict); } diff --git a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java index bd92d883b..fe5225ebd 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java @@ -16,14 +16,15 @@ package com.android.inputmethod.latin.spellcheck; -import com.android.inputmethod.keyboard.KeyDetector; import com.android.inputmethod.keyboard.ProximityInfo; +import com.android.inputmethod.latin.CollectionUtils; +import com.android.inputmethod.latin.Constants; import java.util.TreeMap; public class SpellCheckerProximityInfo { /* public for test */ - final public static int NUL = KeyDetector.NOT_A_CODE; + final public static int NUL = Constants.NOT_A_CODE; // This must be the same as MAX_PROXIMITY_CHARS_SIZE else it will not work inside // native code - this value is passed at creation of the binary object and reused @@ -59,7 +60,7 @@ public class SpellCheckerProximityInfo { // character. // Since we need to build such an array, we want to be able to search in our big proximity // data quickly by character, and a map is probably the best way to do this. - final private static TreeMap<Integer, Integer> INDICES = new TreeMap<Integer, Integer>(); + final private static TreeMap<Integer, Integer> INDICES = CollectionUtils.newTreeMap(); // The proximity here is the union of // - the proximity for a QWERTY keyboard. @@ -122,7 +123,7 @@ public class SpellCheckerProximityInfo { } private static class Cyrillic { - final private static TreeMap<Integer, Integer> INDICES = new TreeMap<Integer, Integer>(); + final private static TreeMap<Integer, Integer> INDICES = CollectionUtils.newTreeMap(); // TODO: The following table is solely based on the keyboard layout. Consult with Russian // speakers on commonly misspelled words/letters. final static int[] PROXIMITY = { diff --git a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java index 58b01aa55..1f883aa60 100644 --- a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java +++ b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java @@ -23,7 +23,9 @@ import android.graphics.drawable.Drawable; import com.android.inputmethod.keyboard.Key; import com.android.inputmethod.keyboard.Keyboard; import com.android.inputmethod.keyboard.KeyboardSwitcher; +import com.android.inputmethod.keyboard.internal.KeyboardBuilder; import com.android.inputmethod.keyboard.internal.KeyboardIconsSet; +import com.android.inputmethod.keyboard.internal.KeyboardParams; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.SuggestedWords; import com.android.inputmethod.latin.Utils; @@ -31,145 +33,149 @@ import com.android.inputmethod.latin.Utils; public class MoreSuggestions extends Keyboard { public static final int SUGGESTION_CODE_BASE = 1024; - MoreSuggestions(Builder.MoreSuggestionsParam params) { + MoreSuggestions(final MoreSuggestionsParam params) { super(params); } - public static class Builder extends Keyboard.Builder<Builder.MoreSuggestionsParam> { - private final MoreSuggestionsView mPaneView; - private SuggestedWords mSuggestions; - private int mFromPos; - private int mToPos; + private static class MoreSuggestionsParam extends KeyboardParams { + private final int[] mWidths = new int[SuggestionStripView.MAX_SUGGESTIONS]; + private final int[] mRowNumbers = new int[SuggestionStripView.MAX_SUGGESTIONS]; + private final int[] mColumnOrders = new int[SuggestionStripView.MAX_SUGGESTIONS]; + private final int[] mNumColumnsInRow = new int[SuggestionStripView.MAX_SUGGESTIONS]; + private static final int MAX_COLUMNS_IN_ROW = 3; + private int mNumRows; + public Drawable mDivider; + public int mDividerWidth; + + public MoreSuggestionsParam() { + super(); + } - public static class MoreSuggestionsParam extends Keyboard.Params { - private final int[] mWidths = new int[SuggestionStripView.MAX_SUGGESTIONS]; - private final int[] mRowNumbers = new int[SuggestionStripView.MAX_SUGGESTIONS]; - private final int[] mColumnOrders = new int[SuggestionStripView.MAX_SUGGESTIONS]; - private final int[] mNumColumnsInRow = new int[SuggestionStripView.MAX_SUGGESTIONS]; - private static final int MAX_COLUMNS_IN_ROW = 3; - private int mNumRows; - public Drawable mDivider; - public int mDividerWidth; - - public int layout(SuggestedWords suggestions, int fromPos, int maxWidth, int minWidth, - int maxRow, MoreSuggestionsView view) { - clearKeys(); - final Resources res = view.getContext().getResources(); - mDivider = res.getDrawable(R.drawable.more_suggestions_divider); - mDividerWidth = mDivider.getIntrinsicWidth(); - final int padding = (int) res.getDimension( - R.dimen.more_suggestions_key_horizontal_padding); - final Paint paint = view.newDefaultLabelPaint(); - - int row = 0; - int pos = fromPos, rowStartPos = fromPos; - final int size = Math.min(suggestions.size(), SuggestionStripView.MAX_SUGGESTIONS); - while (pos < size) { - final String word = suggestions.getWord(pos).toString(); - // TODO: Should take care of text x-scaling. - mWidths[pos] = (int)view.getLabelWidth(word, paint) + padding; - final int numColumn = pos - rowStartPos + 1; - final int columnWidth = - (maxWidth - mDividerWidth * (numColumn - 1)) / numColumn; - if (numColumn > MAX_COLUMNS_IN_ROW - || !fitInWidth(rowStartPos, pos + 1, columnWidth)) { - if ((row + 1) >= maxRow) { - break; - } - mNumColumnsInRow[row] = pos - rowStartPos; - rowStartPos = pos; - row++; + public int layout(final SuggestedWords suggestions, final int fromPos, final int maxWidth, + final int minWidth, final int maxRow, final MoreSuggestionsView view) { + clearKeys(); + final Resources res = view.getContext().getResources(); + mDivider = res.getDrawable(R.drawable.more_suggestions_divider); + mDividerWidth = mDivider.getIntrinsicWidth(); + final int padding = (int) res.getDimension( + R.dimen.more_suggestions_key_horizontal_padding); + final Paint paint = view.newDefaultLabelPaint(); + + int row = 0; + int pos = fromPos, rowStartPos = fromPos; + final int size = Math.min(suggestions.size(), SuggestionStripView.MAX_SUGGESTIONS); + while (pos < size) { + final String word = suggestions.getWord(pos).toString(); + // TODO: Should take care of text x-scaling. + mWidths[pos] = (int)view.getLabelWidth(word, paint) + padding; + final int numColumn = pos - rowStartPos + 1; + final int columnWidth = + (maxWidth - mDividerWidth * (numColumn - 1)) / numColumn; + if (numColumn > MAX_COLUMNS_IN_ROW + || !fitInWidth(rowStartPos, pos + 1, columnWidth)) { + if ((row + 1) >= maxRow) { + break; } - mColumnOrders[pos] = pos - rowStartPos; - mRowNumbers[pos] = row; - pos++; + mNumColumnsInRow[row] = pos - rowStartPos; + rowStartPos = pos; + row++; } - mNumColumnsInRow[row] = pos - rowStartPos; - mNumRows = row + 1; - mBaseWidth = mOccupiedWidth = Math.max( - minWidth, calcurateMaxRowWidth(fromPos, pos)); - mBaseHeight = mOccupiedHeight = mNumRows * mDefaultRowHeight + mVerticalGap; - return pos - fromPos; + mColumnOrders[pos] = pos - rowStartPos; + mRowNumbers[pos] = row; + pos++; } + mNumColumnsInRow[row] = pos - rowStartPos; + mNumRows = row + 1; + mBaseWidth = mOccupiedWidth = Math.max( + minWidth, calcurateMaxRowWidth(fromPos, pos)); + mBaseHeight = mOccupiedHeight = mNumRows * mDefaultRowHeight + mVerticalGap; + return pos - fromPos; + } - private boolean fitInWidth(int startPos, int endPos, int width) { - for (int pos = startPos; pos < endPos; pos++) { - if (mWidths[pos] > width) - return false; - } - return true; + private boolean fitInWidth(final int startPos, final int endPos, final int width) { + for (int pos = startPos; pos < endPos; pos++) { + if (mWidths[pos] > width) + return false; } + return true; + } - private int calcurateMaxRowWidth(int startPos, int endPos) { - int maxRowWidth = 0; - int pos = startPos; - for (int row = 0; row < mNumRows; row++) { - final int numColumnInRow = mNumColumnsInRow[row]; - int maxKeyWidth = 0; - while (pos < endPos && mRowNumbers[pos] == row) { - maxKeyWidth = Math.max(maxKeyWidth, mWidths[pos]); - pos++; - } - maxRowWidth = Math.max(maxRowWidth, - maxKeyWidth * numColumnInRow + mDividerWidth * (numColumnInRow - 1)); + private int calcurateMaxRowWidth(final int startPos, final int endPos) { + int maxRowWidth = 0; + int pos = startPos; + for (int row = 0; row < mNumRows; row++) { + final int numColumnInRow = mNumColumnsInRow[row]; + int maxKeyWidth = 0; + while (pos < endPos && mRowNumbers[pos] == row) { + maxKeyWidth = Math.max(maxKeyWidth, mWidths[pos]); + pos++; } - return maxRowWidth; + maxRowWidth = Math.max(maxRowWidth, + maxKeyWidth * numColumnInRow + mDividerWidth * (numColumnInRow - 1)); } + return maxRowWidth; + } - private static final int[][] COLUMN_ORDER_TO_NUMBER = { - { 0, }, - { 1, 0, }, - { 2, 0, 1}, - }; - - public int getNumColumnInRow(int pos) { - return mNumColumnsInRow[mRowNumbers[pos]]; - } + private static final int[][] COLUMN_ORDER_TO_NUMBER = { + { 0, }, + { 1, 0, }, + { 2, 0, 1}, + }; - public int getColumnNumber(int pos) { - final int columnOrder = mColumnOrders[pos]; - final int numColumn = getNumColumnInRow(pos); - return COLUMN_ORDER_TO_NUMBER[numColumn - 1][columnOrder]; - } + public int getNumColumnInRow(final int pos) { + return mNumColumnsInRow[mRowNumbers[pos]]; + } - public int getX(int pos) { - final int columnNumber = getColumnNumber(pos); - return columnNumber * (getWidth(pos) + mDividerWidth); - } + public int getColumnNumber(final int pos) { + final int columnOrder = mColumnOrders[pos]; + final int numColumn = getNumColumnInRow(pos); + return COLUMN_ORDER_TO_NUMBER[numColumn - 1][columnOrder]; + } - public int getY(int pos) { - final int row = mRowNumbers[pos]; - return (mNumRows -1 - row) * mDefaultRowHeight + mTopPadding; - } + public int getX(final int pos) { + final int columnNumber = getColumnNumber(pos); + return columnNumber * (getWidth(pos) + mDividerWidth); + } - public int getWidth(int pos) { - final int numColumnInRow = getNumColumnInRow(pos); - return (mOccupiedWidth - mDividerWidth * (numColumnInRow - 1)) / numColumnInRow; - } + public int getY(final int pos) { + final int row = mRowNumbers[pos]; + return (mNumRows -1 - row) * mDefaultRowHeight + mTopPadding; + } - public void markAsEdgeKey(Key key, int pos) { - final int row = mRowNumbers[pos]; - if (row == 0) - key.markAsBottomEdge(this); - if (row == mNumRows - 1) - key.markAsTopEdge(this); + public int getWidth(final int pos) { + final int numColumnInRow = getNumColumnInRow(pos); + return (mOccupiedWidth - mDividerWidth * (numColumnInRow - 1)) / numColumnInRow; + } - final int numColumnInRow = mNumColumnsInRow[row]; - final int column = getColumnNumber(pos); - if (column == 0) - key.markAsLeftEdge(this); - if (column == numColumnInRow - 1) - key.markAsRightEdge(this); - } + public void markAsEdgeKey(final Key key, final int pos) { + final int row = mRowNumbers[pos]; + if (row == 0) + key.markAsBottomEdge(this); + if (row == mNumRows - 1) + key.markAsTopEdge(this); + + final int numColumnInRow = mNumColumnsInRow[row]; + final int column = getColumnNumber(pos); + if (column == 0) + key.markAsLeftEdge(this); + if (column == numColumnInRow - 1) + key.markAsRightEdge(this); } + } - public Builder(MoreSuggestionsView paneView) { + public static class Builder extends KeyboardBuilder<MoreSuggestionsParam> { + private final MoreSuggestionsView mPaneView; + private SuggestedWords mSuggestions; + private int mFromPos; + private int mToPos; + + public Builder(final MoreSuggestionsView paneView) { super(paneView.getContext(), new MoreSuggestionsParam()); mPaneView = paneView; } - public Builder layout(SuggestedWords suggestions, int fromPos, int maxWidth, - int minWidth, int maxRow) { + public Builder layout(final SuggestedWords suggestions, final int fromPos, + final int maxWidth, final int minWidth, final int maxRow) { final Keyboard keyboard = KeyboardSwitcher.getInstance().getKeyboard(); final int xmlId = R.xml.kbd_suggestions_pane_template; load(xmlId, keyboard.mId); @@ -183,25 +189,6 @@ public class MoreSuggestions extends Keyboard { return this; } - private static class Divider extends Key.Spacer { - private final Drawable mIcon; - - public Divider(Keyboard.Params params, Drawable icon, int x, int y, int width, - int height) { - super(params, x, y, width, height); - mIcon = icon; - } - - @Override - public Drawable getIcon(KeyboardIconsSet iconSet, int alpha) { - // KeyboardIconsSet and alpha are unused. Use the icon that has been passed to the - // constructor. - // TODO: Drawable itself should have an alpha value. - mIcon.setAlpha(128); - return mIcon; - } - } - @Override public MoreSuggestions build() { final MoreSuggestionsParam params = mParams; @@ -228,4 +215,23 @@ public class MoreSuggestions extends Keyboard { return new MoreSuggestions(params); } } + + private static class Divider extends Key.Spacer { + private final Drawable mIcon; + + public Divider(final KeyboardParams params, final Drawable icon, final int x, + final int y, final int width, final int height) { + super(params, x, y, width, height); + mIcon = icon; + } + + @Override + public Drawable getIcon(final KeyboardIconsSet iconSet, final int alpha) { + // KeyboardIconsSet and alpha are unused. Use the icon that has been passed to the + // constructor. + // TODO: Drawable itself should have an alpha value. + mIcon.setAlpha(128); + return mIcon; + } + } } diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java index b57ffd2de..9e8ab81b0 100644 --- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java +++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java @@ -58,8 +58,10 @@ import com.android.inputmethod.keyboard.MoreKeysPanel; import com.android.inputmethod.keyboard.PointerTracker; import com.android.inputmethod.keyboard.ViewLayoutUtils; import com.android.inputmethod.latin.AutoCorrection; +import com.android.inputmethod.latin.CollectionUtils; import com.android.inputmethod.latin.LatinImeLogger; import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.ResourceUtils; import com.android.inputmethod.latin.StaticInnerHandlerWrapper; import com.android.inputmethod.latin.SuggestedWords; import com.android.inputmethod.latin.Utils; @@ -72,7 +74,7 @@ public class SuggestionStripView extends RelativeLayout implements OnClickListen OnLongClickListener { public interface Listener { public boolean addWordToUserDictionary(String word); - public void pickSuggestionManually(int index, CharSequence word, int x, int y); + public void pickSuggestionManually(int index, CharSequence word); } // The maximum number of suggestions available. See {@link Suggest#mPrefMaxSuggestions}. @@ -88,9 +90,9 @@ public class SuggestionStripView extends RelativeLayout implements OnClickListen private final MoreSuggestions.Builder mMoreSuggestionsBuilder; private final PopupWindow mMoreSuggestionsWindow; - private final ArrayList<TextView> mWords = new ArrayList<TextView>(); - private final ArrayList<TextView> mInfos = new ArrayList<TextView>(); - private final ArrayList<View> mDividers = new ArrayList<View>(); + private final ArrayList<TextView> mWords = CollectionUtils.newArrayList(); + private final ArrayList<TextView> mInfos = CollectionUtils.newArrayList(); + private final ArrayList<View> mDividers = CollectionUtils.newArrayList(); private final PopupWindow mPreviewPopup; private final TextView mPreviewText; @@ -131,7 +133,7 @@ public class SuggestionStripView extends RelativeLayout implements OnClickListen private static class SuggestionStripViewParams { private static final int DEFAULT_SUGGESTIONS_COUNT_IN_STRIP = 3; - private static final int DEFAULT_CENTER_SUGGESTION_PERCENTILE = 40; + private static final float DEFAULT_CENTER_SUGGESTION_PERCENTILE = 0.40f; private static final int DEFAULT_MAX_MORE_SUGGESTIONS_ROW = 2; private static final int PUNCTUATIONS_IN_STRIP = 5; @@ -167,7 +169,7 @@ public class SuggestionStripView extends RelativeLayout implements OnClickListen private final int mSuggestionStripOption; - private final ArrayList<CharSequence> mTexts = new ArrayList<CharSequence>(); + private final ArrayList<CharSequence> mTexts = CollectionUtils.newArrayList(); public boolean mMoreSuggestionsAvailable; @@ -195,16 +197,16 @@ public class SuggestionStripView extends RelativeLayout implements OnClickListen R.styleable.SuggestionStripView, defStyle, R.style.SuggestionStripViewStyle); mSuggestionStripOption = a.getInt( R.styleable.SuggestionStripView_suggestionStripOption, 0); - final float alphaValidTypedWord = getPercent(a, - R.styleable.SuggestionStripView_alphaValidTypedWord, 100); - final float alphaTypedWord = getPercent(a, - R.styleable.SuggestionStripView_alphaTypedWord, 100); - final float alphaAutoCorrect = getPercent(a, - R.styleable.SuggestionStripView_alphaAutoCorrect, 100); - final float alphaSuggested = getPercent(a, - R.styleable.SuggestionStripView_alphaSuggested, 100); - mAlphaObsoleted = getPercent(a, - R.styleable.SuggestionStripView_alphaSuggested, 100); + final float alphaValidTypedWord = ResourceUtils.getFraction(a, + R.styleable.SuggestionStripView_alphaValidTypedWord, 1.0f); + final float alphaTypedWord = ResourceUtils.getFraction(a, + R.styleable.SuggestionStripView_alphaTypedWord, 1.0f); + final float alphaAutoCorrect = ResourceUtils.getFraction(a, + R.styleable.SuggestionStripView_alphaAutoCorrect, 1.0f); + final float alphaSuggested = ResourceUtils.getFraction(a, + R.styleable.SuggestionStripView_alphaSuggested, 1.0f); + mAlphaObsoleted = ResourceUtils.getFraction(a, + R.styleable.SuggestionStripView_alphaSuggested, 1.0f); mColorValidTypedWord = applyAlpha(a.getColor( R.styleable.SuggestionStripView_colorValidTypedWord, 0), alphaValidTypedWord); mColorTypedWord = applyAlpha(a.getColor( @@ -216,14 +218,14 @@ public class SuggestionStripView extends RelativeLayout implements OnClickListen mSuggestionsCountInStrip = a.getInt( R.styleable.SuggestionStripView_suggestionsCountInStrip, DEFAULT_SUGGESTIONS_COUNT_IN_STRIP); - mCenterSuggestionWeight = getPercent(a, + mCenterSuggestionWeight = ResourceUtils.getFraction(a, R.styleable.SuggestionStripView_centerSuggestionPercentile, DEFAULT_CENTER_SUGGESTION_PERCENTILE); mMaxMoreSuggestionsRow = a.getInt( R.styleable.SuggestionStripView_maxMoreSuggestionsRow, DEFAULT_MAX_MORE_SUGGESTIONS_ROW); - mMinMoreSuggestionsWidth = getRatio(a, - R.styleable.SuggestionStripView_minMoreSuggestionsWidth); + mMinMoreSuggestionsWidth = ResourceUtils.getFraction(a, + R.styleable.SuggestionStripView_minMoreSuggestionsWidth, 1.0f); a.recycle(); mMoreSuggestionsHint = getMoreSuggestionsHint(res, @@ -277,16 +279,6 @@ public class SuggestionStripView extends RelativeLayout implements OnClickListen return new BitmapDrawable(res, buffer); } - // Read integer value in TypedArray as percent. - private static float getPercent(TypedArray a, int index, int defValue) { - return a.getInt(index, defValue) / 100.0f; - } - - // Read fraction value in TypedArray as float. - private static float getRatio(TypedArray a, int index) { - return a.getFraction(index, 1000, 1000, 1) / 1000.0f; - } - private CharSequence getStyledSuggestionWord(SuggestedWords suggestedWords, int pos) { final CharSequence word = suggestedWords.getWord(pos); final boolean isAutoCorrect = pos == 1 && suggestedWords.willAutoCorrect(); @@ -726,9 +718,7 @@ public class SuggestionStripView extends RelativeLayout implements OnClickListen public boolean onCustomRequest(int requestCode) { final int index = requestCode; final CharSequence word = mSuggestedWords.getWord(index); - // TODO: change caller path so coordinates are passed through here - mListener.pickSuggestionManually(index, word, NOT_A_TOUCH_COORDINATE, - NOT_A_TOUCH_COORDINATE); + mListener.pickSuggestionManually(index, word); dismissMoreSuggestions(); return true; } @@ -874,7 +864,7 @@ public class SuggestionStripView extends RelativeLayout implements OnClickListen return; final CharSequence word = mSuggestedWords.getWord(index); - mListener.pickSuggestionManually(index, word, mLastX, mLastY); + mListener.pickSuggestionManually(index, word); } @Override diff --git a/java/src/com/android/inputmethod/research/BootBroadcastReceiver.java b/java/src/com/android/inputmethod/research/BootBroadcastReceiver.java new file mode 100644 index 000000000..5124a35a6 --- /dev/null +++ b/java/src/com/android/inputmethod/research/BootBroadcastReceiver.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.inputmethod.research; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +/** + * Arrange for the uploading service to be run on regular intervals. + */ +public final class BootBroadcastReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) { + ResearchLogger.scheduleUploadingService(context); + } + } +} diff --git a/java/src/com/android/inputmethod/research/FeedbackActivity.java b/java/src/com/android/inputmethod/research/FeedbackActivity.java index c9f3b476a..11eae8813 100644 --- a/java/src/com/android/inputmethod/research/FeedbackActivity.java +++ b/java/src/com/android/inputmethod/research/FeedbackActivity.java @@ -18,10 +18,7 @@ package com.android.inputmethod.research; import android.app.Activity; import android.os.Bundle; -import android.text.Editable; -import android.view.View; import android.widget.CheckBox; -import android.widget.EditText; import com.android.inputmethod.latin.R; @@ -31,6 +28,11 @@ 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); } diff --git a/java/src/com/android/inputmethod/research/LogBuffer.java b/java/src/com/android/inputmethod/research/LogBuffer.java new file mode 100644 index 000000000..ae7b1579a --- /dev/null +++ b/java/src/com/android/inputmethod/research/LogBuffer.java @@ -0,0 +1,113 @@ +/* + * 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 com.android.inputmethod.latin.CollectionUtils; + +import java.util.LinkedList; + +/** + * A buffer that holds a fixed number of LogUnits. + * + * LogUnits are added in and shifted out in temporal order. Only a subset of the LogUnits are + * actual words; the other LogUnits do not count toward the word limit. Once the buffer reaches + * capacity, adding another LogUnit that is a word evicts the oldest LogUnits out one at a time to + * stay under the capacity limit. + */ +public class LogBuffer { + protected final LinkedList<LogUnit> mLogUnits; + /* package for test */ int mWordCapacity; + // The number of members of mLogUnits that are actual words. + protected int mNumActualWords; + + /** + * Create a new LogBuffer that can hold a fixed number of LogUnits that are words (and + * unlimited number of non-word LogUnits), and that outputs its result to a researchLog. + * + * @param wordCapacity maximum number of words + */ + LogBuffer(final int wordCapacity) { + if (wordCapacity <= 0) { + throw new IllegalArgumentException("wordCapacity must be 1 or greater."); + } + mLogUnits = CollectionUtils.newLinkedList(); + mWordCapacity = wordCapacity; + mNumActualWords = 0; + } + + /** + * Adds a new LogUnit to the front of the LIFO queue, evicting existing LogUnit's + * (oldest first) if word capacity is reached. + */ + public void shiftIn(LogUnit newLogUnit) { + if (newLogUnit.getWord() == null) { + // This LogUnit isn't a word, so it doesn't count toward the word-limit. + mLogUnits.add(newLogUnit); + return; + } + if (mNumActualWords == mWordCapacity) { + shiftOutThroughFirstWord(); + } + mLogUnits.add(newLogUnit); + mNumActualWords++; // Must be a word, or we wouldn't be here. + } + + private void shiftOutThroughFirstWord() { + while (!mLogUnits.isEmpty()) { + final LogUnit logUnit = mLogUnits.removeFirst(); + onShiftOut(logUnit); + if (logUnit.hasWord()) { + // Successfully shifted out a word-containing LogUnit and made space for the new + // LogUnit. + mNumActualWords--; + break; + } + } + } + + /** + * Removes all LogUnits from the buffer without calling onShiftOut(). + */ + public void clear() { + mLogUnits.clear(); + mNumActualWords = 0; + } + + /** + * Called when a LogUnit is removed from the LogBuffer as a result of a shiftIn. LogUnits are + * removed in the order entered. This method is not called when shiftOut is called directly. + * + * Base class does nothing; subclasses may override. + */ + protected void onShiftOut(LogUnit logUnit) { + } + + /** + * Called to deliberately remove the oldest LogUnit. Usually called when draining the + * LogBuffer. + */ + public LogUnit shiftOut() { + if (mLogUnits.isEmpty()) { + return null; + } + final LogUnit logUnit = mLogUnits.removeFirst(); + if (logUnit.hasWord()) { + mNumActualWords--; + } + return logUnit; + } +} diff --git a/java/src/com/android/inputmethod/research/LogUnit.java b/java/src/com/android/inputmethod/research/LogUnit.java new file mode 100644 index 000000000..d8b3a29ff --- /dev/null +++ b/java/src/com/android/inputmethod/research/LogUnit.java @@ -0,0 +1,83 @@ +/* + * 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 com.android.inputmethod.latin.CollectionUtils; + +import java.util.ArrayList; + +/** + * A group of log statements related to each other. + * + * A LogUnit is collection of LogStatements, each of which is generated by at a particular point + * in the code. (There is no LogStatement class; the data is stored across the instance variables + * here.) A single LogUnit's statements can correspond to all the calls made while in the same + * composing region, or all the calls between committing the last composing region, and the first + * character of the next composing region. + * + * Individual statements in a log may be marked as potentially private. If so, then they are only + * published to a ResearchLog if the ResearchLogger determines that publishing the entire LogUnit + * will not violate the user's privacy. Checks for this may include whether other LogUnits have + * been published recently, or whether the LogUnit contains numbers, etc. + */ +/* package */ class LogUnit { + private final ArrayList<String[]> mKeysList = CollectionUtils.newArrayList(); + private final ArrayList<Object[]> mValuesList = CollectionUtils.newArrayList(); + private final ArrayList<Boolean> mIsPotentiallyPrivate = CollectionUtils.newArrayList(); + private String mWord; + private boolean mContainsDigit; + + public void addLogStatement(final String[] keys, final Object[] values, + final Boolean isPotentiallyPrivate) { + mKeysList.add(keys); + mValuesList.add(values); + mIsPotentiallyPrivate.add(isPotentiallyPrivate); + } + + public void publishTo(final ResearchLog researchLog, final boolean isIncludingPrivateData) { + final int size = mKeysList.size(); + for (int i = 0; i < size; i++) { + if (!mIsPotentiallyPrivate.get(i) || isIncludingPrivateData) { + researchLog.outputEvent(mKeysList.get(i), mValuesList.get(i)); + } + } + } + + public void setWord(String word) { + mWord = word; + } + + public String getWord() { + return mWord; + } + + public boolean hasWord() { + return mWord != null; + } + + public void setContainsDigit() { + mContainsDigit = true; + } + + public boolean hasDigit() { + return mContainsDigit; + } + + public boolean isEmpty() { + return mKeysList.isEmpty(); + } +} diff --git a/java/src/com/android/inputmethod/research/MainLogBuffer.java b/java/src/com/android/inputmethod/research/MainLogBuffer.java new file mode 100644 index 000000000..745768d35 --- /dev/null +++ b/java/src/com/android/inputmethod/research/MainLogBuffer.java @@ -0,0 +1,127 @@ +/* + * 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 com.android.inputmethod.latin.Dictionary; +import com.android.inputmethod.latin.Suggest; + +import java.util.Random; + +public class MainLogBuffer extends LogBuffer { + // The size of the n-grams logged. E.g. N_GRAM_SIZE = 2 means to sample bigrams. + private static final int N_GRAM_SIZE = 2; + // The number of words between n-grams to omit from the log. + private static final int DEFAULT_NUMBER_OF_WORDS_BETWEEN_SAMPLES = 18; + + private final ResearchLog mResearchLog; + private Suggest mSuggest; + + // The minimum periodicity with which n-grams can be sampled. E.g. mWinWordPeriod is 10 if + // every 10th bigram is sampled, i.e., words 1-8 are not, but the bigram at words 9 and 10, etc. + // for 11-18, and the bigram at words 19 and 20. If an n-gram is not safe (e.g. it contains a + // number in the middle or an out-of-vocabulary word), then sampling is delayed until a safe + // n-gram does appear. + /* package for test */ int mMinWordPeriod; + + // Counter for words left to suppress before an n-gram can be sampled. Reset to mMinWordPeriod + // after a sample is taken. + /* package for test */ int mWordsUntilSafeToSample; + + public MainLogBuffer(final ResearchLog researchLog) { + super(N_GRAM_SIZE); + mResearchLog = researchLog; + mMinWordPeriod = DEFAULT_NUMBER_OF_WORDS_BETWEEN_SAMPLES + N_GRAM_SIZE; + final Random random = new Random(); + mWordsUntilSafeToSample = random.nextInt(mMinWordPeriod); + } + + public void setSuggest(Suggest suggest) { + mSuggest = suggest; + } + + @Override + public void shiftIn(final LogUnit newLogUnit) { + super.shiftIn(newLogUnit); + if (newLogUnit.hasWord()) { + if (mWordsUntilSafeToSample > 0) { + mWordsUntilSafeToSample--; + } + } + } + + public void resetWordCounter() { + mWordsUntilSafeToSample = mMinWordPeriod; + } + + /** + * Determines whether the content of the MainLogBuffer can be safely uploaded in its complete + * form and still protect the user's privacy. + * + * The size of the MainLogBuffer is just enough to hold one n-gram, its corrections, and any + * non-character data that is typed between words. The decision about privacy is made based on + * the buffer's entire content. If it is decided that the privacy risks are too great to upload + * the contents of this buffer, a censored version of the LogItems may still be uploaded. E.g., + * the screen orientation and other characteristics about the device can be uploaded without + * revealing much about the user. + */ + public boolean isSafeToLog() { + // Check that we are not sampling too frequently. Having sampled recently might disclose + // too much of the user's intended meaning. + if (mWordsUntilSafeToSample > 0) { + return false; + } + if (mSuggest == null || !mSuggest.hasMainDictionary()) { + // Main dictionary is unavailable. Since we cannot check it, we cannot tell if a word + // is out-of-vocabulary or not. Therefore, we must judge the entire buffer contents to + // potentially pose a privacy risk. + return false; + } + // Reload the dictionary in case it has changed (e.g., because the user has changed + // languages). + final Dictionary dictionary = mSuggest.getMainDictionary(); + if (dictionary == null) { + return false; + } + // Check each word in the buffer. If any word poses a privacy threat, we cannot upload the + // complete buffer contents in detail. + final int length = mLogUnits.size(); + for (int i = 0; i < length; i++) { + final LogUnit logUnit = mLogUnits.get(i); + final String word = logUnit.getWord(); + if (word == null) { + // Digits outside words are a privacy threat. + if (logUnit.hasDigit()) { + return false; + } + } else { + // Words not in the dictionary are a privacy threat. + if (!(dictionary.isValidWord(word))) { + return false; + } + } + } + // All checks have passed; this buffer's content can be safely uploaded. + return true; + } + + @Override + protected void onShiftOut(LogUnit logUnit) { + if (mResearchLog != null) { + mResearchLog.publish(logUnit, false /* isIncludingPrivateData */); + } + } +} diff --git a/java/src/com/android/inputmethod/research/ResearchLog.java b/java/src/com/android/inputmethod/research/ResearchLog.java index 18bf3c07f..cd9ff85f8 100644 --- a/java/src/com/android/inputmethod/research/ResearchLog.java +++ b/java/src/com/android/inputmethod/research/ResearchLog.java @@ -26,7 +26,6 @@ import com.android.inputmethod.keyboard.Key; import com.android.inputmethod.latin.SuggestedWords; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.define.ProductionFlag; -import com.android.inputmethod.research.ResearchLogger.LogUnit; import java.io.BufferedWriter; import java.io.File; @@ -37,6 +36,7 @@ import java.io.OutputStreamWriter; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.Executors; +import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -51,21 +51,22 @@ import java.util.concurrent.TimeUnit; */ public class ResearchLog { private static final String TAG = ResearchLog.class.getSimpleName(); - private static final JsonWriter NULL_JSON_WRITER = new JsonWriter( - new OutputStreamWriter(new NullOutputStream())); + private static final boolean DEBUG = false; + private static final long FLUSH_DELAY_IN_MS = 1000 * 5; + private static final int ABORT_TIMEOUT_IN_MS = 1000 * 4; - final ScheduledExecutorService mExecutor; + /* package */ final ScheduledExecutorService mExecutor; /* package */ final File mFile; private JsonWriter mJsonWriter = NULL_JSON_WRITER; + // true if at least one byte of data has been written out to the log file. This must be + // remembered because JsonWriter requires that calls matching calls to beginObject and + // endObject, as well as beginArray and endArray, and the file is opened lazily, only when + // it is certain that data will be written. Alternatively, the matching call exceptions + // could be caught, but this might suppress other errors. + private boolean mHasWrittenData = false; - private int mLoggingState; - private static final int LOGGING_STATE_UNSTARTED = 0; - private static final int LOGGING_STATE_READY = 1; // don't create file until necessary - private static final int LOGGING_STATE_RUNNING = 2; - private static final int LOGGING_STATE_STOPPING = 3; - private static final int LOGGING_STATE_STOPPED = 4; - private static final long FLUSH_DELAY_IN_MS = 1000 * 5; - + private static final JsonWriter NULL_JSON_WRITER = new JsonWriter( + new OutputStreamWriter(new NullOutputStream())); private static class NullOutputStream extends OutputStream { /** {@inheritDoc} */ @Override @@ -84,128 +85,81 @@ public class ResearchLog { } } - public ResearchLog(File outputFile) { - mExecutor = Executors.newSingleThreadScheduledExecutor(); + public ResearchLog(final File outputFile) { if (outputFile == null) { throw new IllegalArgumentException(); } + mExecutor = Executors.newSingleThreadScheduledExecutor(); mFile = outputFile; - mLoggingState = LOGGING_STATE_UNSTARTED; - } - - public synchronized void start() throws IOException { - switch (mLoggingState) { - case LOGGING_STATE_UNSTARTED: - mLoggingState = LOGGING_STATE_READY; - break; - case LOGGING_STATE_READY: - case LOGGING_STATE_RUNNING: - case LOGGING_STATE_STOPPING: - case LOGGING_STATE_STOPPED: - break; - } } - public synchronized void stop() { - switch (mLoggingState) { - case LOGGING_STATE_UNSTARTED: - mLoggingState = LOGGING_STATE_STOPPED; - break; - case LOGGING_STATE_READY: - case LOGGING_STATE_RUNNING: - mExecutor.submit(new Callable<Object>() { - @Override - public Object call() throws Exception { - try { - mJsonWriter.endArray(); - mJsonWriter.flush(); - mJsonWriter.close(); - } finally { - boolean success = mFile.setWritable(false, false); - mLoggingState = LOGGING_STATE_STOPPED; - } - return null; + public synchronized void close() { + mExecutor.submit(new Callable<Object>() { + @Override + public Object call() throws Exception { + try { + if (mHasWrittenData) { + mJsonWriter.endArray(); + mJsonWriter.flush(); + mJsonWriter.close(); + mHasWrittenData = false; } - }); - removeAnyScheduledFlush(); - mExecutor.shutdown(); - mLoggingState = LOGGING_STATE_STOPPING; - break; - case LOGGING_STATE_STOPPING: - case LOGGING_STATE_STOPPED: - } + } catch (Exception e) { + Log.d(TAG, "error when closing ResearchLog:"); + e.printStackTrace(); + } finally { + if (mFile.exists()) { + mFile.setWritable(false, false); + } + } + return null; + } + }); + removeAnyScheduledFlush(); + mExecutor.shutdown(); } - public boolean isAlive() { - switch (mLoggingState) { - case LOGGING_STATE_UNSTARTED: - case LOGGING_STATE_READY: - case LOGGING_STATE_RUNNING: - return true; - } - return false; - } + private boolean mIsAbortSuccessful; - public void waitUntilStopped(final int timeoutInMs) throws InterruptedException { + public synchronized void abort() { + mExecutor.submit(new Callable<Object>() { + @Override + public Object call() throws Exception { + try { + if (mHasWrittenData) { + mJsonWriter.endArray(); + mJsonWriter.close(); + mHasWrittenData = false; + } + } finally { + mIsAbortSuccessful = mFile.delete(); + } + return null; + } + }); removeAnyScheduledFlush(); mExecutor.shutdown(); - mExecutor.awaitTermination(timeoutInMs, TimeUnit.MILLISECONDS); } - public synchronized void abort() { - switch (mLoggingState) { - case LOGGING_STATE_UNSTARTED: - mLoggingState = LOGGING_STATE_STOPPED; - isAbortSuccessful = true; - break; - case LOGGING_STATE_READY: - case LOGGING_STATE_RUNNING: - mExecutor.submit(new Callable<Object>() { - @Override - public Object call() throws Exception { - try { - mJsonWriter.endArray(); - mJsonWriter.close(); - } finally { - isAbortSuccessful = mFile.delete(); - } - return null; - } - }); - removeAnyScheduledFlush(); - mExecutor.shutdown(); - mLoggingState = LOGGING_STATE_STOPPING; - break; - case LOGGING_STATE_STOPPING: - case LOGGING_STATE_STOPPED: - } + public boolean blockingAbort() throws InterruptedException { + abort(); + mExecutor.awaitTermination(ABORT_TIMEOUT_IN_MS, TimeUnit.MILLISECONDS); + return mIsAbortSuccessful; } - private boolean isAbortSuccessful; - public boolean isAbortSuccessful() { - return isAbortSuccessful; + public void awaitTermination(int delay, TimeUnit timeUnit) throws InterruptedException { + mExecutor.awaitTermination(delay, timeUnit); } /* package */ synchronized void flush() { - switch (mLoggingState) { - case LOGGING_STATE_UNSTARTED: - break; - case LOGGING_STATE_READY: - case LOGGING_STATE_RUNNING: - removeAnyScheduledFlush(); - mExecutor.submit(mFlushCallable); - break; - case LOGGING_STATE_STOPPING: - case LOGGING_STATE_STOPPED: - } + removeAnyScheduledFlush(); + mExecutor.submit(mFlushCallable); } - private Callable<Object> mFlushCallable = new Callable<Object>() { + private final Callable<Object> mFlushCallable = new Callable<Object>() { @Override public Object call() throws Exception { - if (mLoggingState == LOGGING_STATE_RUNNING) { - mJsonWriter.flush(); - } + mJsonWriter.flush(); return null; } }; @@ -224,56 +178,40 @@ public class ResearchLog { mFlushFuture = mExecutor.schedule(mFlushCallable, FLUSH_DELAY_IN_MS, TimeUnit.MILLISECONDS); } - public synchronized void publishPublicEvents(final LogUnit logUnit) { - switch (mLoggingState) { - case LOGGING_STATE_UNSTARTED: - break; - case LOGGING_STATE_READY: - case LOGGING_STATE_RUNNING: - mExecutor.submit(new Callable<Object>() { - @Override - public Object call() throws Exception { - logUnit.publishPublicEventsTo(ResearchLog.this); - scheduleFlush(); - return null; - } - }); - break; - case LOGGING_STATE_STOPPING: - case LOGGING_STATE_STOPPED: - } - } - - public synchronized void publishAllEvents(final LogUnit logUnit) { - switch (mLoggingState) { - case LOGGING_STATE_UNSTARTED: - break; - case LOGGING_STATE_READY: - case LOGGING_STATE_RUNNING: - mExecutor.submit(new Callable<Object>() { - @Override - public Object call() throws Exception { - logUnit.publishAllEventsTo(ResearchLog.this); - scheduleFlush(); - return null; - } - }); - break; - case LOGGING_STATE_STOPPING: - case LOGGING_STATE_STOPPED: + public synchronized void publish(final LogUnit logUnit, final boolean isIncludingPrivateData) { + try { + mExecutor.submit(new Callable<Object>() { + @Override + public Object call() throws Exception { + logUnit.publishTo(ResearchLog.this, isIncludingPrivateData); + scheduleFlush(); + return null; + } + }); + } catch (RejectedExecutionException e) { + // TODO: Add code to record loss of data, and report. } } private static final String CURRENT_TIME_KEY = "_ct"; private static final String UPTIME_KEY = "_ut"; private static final String EVENT_TYPE_KEY = "_ty"; + void outputEvent(final String[] keys, final Object[] values) { - // not thread safe. + // Not thread safe. + if (keys.length == 0) { + return; + } + if (DEBUG) { + if (keys.length != values.length + 1) { + Log.d(TAG, "Key and Value list sizes do not match. " + keys[0]); + } + } try { if (mJsonWriter == NULL_JSON_WRITER) { mJsonWriter = new JsonWriter(new BufferedWriter(new FileWriter(mFile))); - mJsonWriter.setLenient(true); mJsonWriter.beginArray(); + mHasWrittenData = true; } mJsonWriter.beginObject(); mJsonWriter.name(CURRENT_TIME_KEY).value(System.currentTimeMillis()); @@ -283,8 +221,8 @@ public class ResearchLog { for (int i = 0; i < length; i++) { mJsonWriter.name(keys[i + 1]); Object value = values[i]; - if (value instanceof String) { - mJsonWriter.value((String) value); + if (value instanceof CharSequence) { + mJsonWriter.value(value.toString()); } else if (value instanceof Number) { mJsonWriter.value((Number) value); } else if (value instanceof Boolean) { @@ -319,7 +257,7 @@ public class ResearchLog { for (Key keyboardKey : keyboardKeys) { mJsonWriter.beginObject(); mJsonWriter.name("code").value(keyboardKey.mCode); - mJsonWriter.name("altCode").value(keyboardKey.mAltCode); + mJsonWriter.name("altCode").value(keyboardKey.getAltCode()); mJsonWriter.name("x").value(keyboardKey.mX); mJsonWriter.name("y").value(keyboardKey.mY); mJsonWriter.name("w").value(keyboardKey.mWidth); @@ -331,14 +269,11 @@ public class ResearchLog { SuggestedWords words = (SuggestedWords) value; mJsonWriter.beginObject(); mJsonWriter.name("typedWordValid").value(words.mTypedWordValid); - mJsonWriter.name("willAutoCorrect") - .value(words.mWillAutoCorrect); + mJsonWriter.name("willAutoCorrect").value(words.mWillAutoCorrect); mJsonWriter.name("isPunctuationSuggestions") - .value(words.mIsPunctuationSuggestions); - mJsonWriter.name("isObsoleteSuggestions") - .value(words.mIsObsoleteSuggestions); - mJsonWriter.name("isPrediction") - .value(words.mIsPrediction); + .value(words.mIsPunctuationSuggestions); + mJsonWriter.name("isObsoleteSuggestions").value(words.mIsObsoleteSuggestions); + mJsonWriter.name("isPrediction").value(words.mIsPrediction); mJsonWriter.name("words"); mJsonWriter.beginArray(); final int size = words.size(); @@ -363,8 +298,8 @@ public class ResearchLog { try { mJsonWriter.close(); } catch (IllegalStateException e1) { - // assume that this is just the json not being terminated properly. - // ignore + // Assume that this is just the json not being terminated properly. + // Ignore } catch (IOException e1) { e1.printStackTrace(); } finally { diff --git a/java/src/com/android/inputmethod/research/ResearchLogUploader.java b/java/src/com/android/inputmethod/research/ResearchLogUploader.java deleted file mode 100644 index 3b1213009..000000000 --- a/java/src/com/android/inputmethod/research/ResearchLogUploader.java +++ /dev/null @@ -1,223 +0,0 @@ -/* - * 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.Manifest; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.PackageManager; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.os.BatteryManager; -import android.util.Log; - -import com.android.inputmethod.latin.R; -import com.android.inputmethod.latin.R.string; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileFilter; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; - -public final class ResearchLogUploader { - private static final String TAG = ResearchLogUploader.class.getSimpleName(); - private static final int UPLOAD_INTERVAL_IN_MS = 1000 * 60 * 15; // every 15 min - private static final int BUF_SIZE = 1024 * 8; - - private final boolean mCanUpload; - private final Context mContext; - private final File mFilesDir; - private final URL mUrl; - private final ScheduledExecutorService mExecutor; - - private Runnable doUploadRunnable = new UploadRunnable(null, false); - - public ResearchLogUploader(final Context context, final File filesDir) { - mContext = context; - mFilesDir = filesDir; - final PackageManager packageManager = context.getPackageManager(); - final boolean hasPermission = packageManager.checkPermission(Manifest.permission.INTERNET, - context.getPackageName()) == PackageManager.PERMISSION_GRANTED; - if (!hasPermission) { - mCanUpload = false; - mUrl = null; - mExecutor = null; - return; - } - URL tempUrl = null; - boolean canUpload = false; - ScheduledExecutorService executor = null; - try { - final String urlString = context.getString(R.string.research_logger_upload_url); - if (urlString == null || urlString.equals("")) { - return; - } - tempUrl = new URL(urlString); - canUpload = true; - executor = Executors.newSingleThreadScheduledExecutor(); - } catch (MalformedURLException e) { - tempUrl = null; - e.printStackTrace(); - return; - } finally { - mCanUpload = canUpload; - mUrl = tempUrl; - mExecutor = executor; - } - } - - public void start() { - if (mCanUpload) { - Log.d(TAG, "scheduling regular uploading"); - mExecutor.scheduleWithFixedDelay(doUploadRunnable, UPLOAD_INTERVAL_IN_MS, - UPLOAD_INTERVAL_IN_MS, TimeUnit.MILLISECONDS); - } else { - Log.d(TAG, "no permission to upload"); - } - } - - public void uploadNow(final Callback callback) { - // Perform an immediate upload. Note that this should happen even if there is - // another upload happening right now, as it may have missed the latest changes. - // TODO: Reschedule regular upload tests starting from now. - if (mCanUpload) { - mExecutor.submit(new UploadRunnable(callback, true)); - } - } - - public interface Callback { - public void onUploadCompleted(final boolean success); - } - - private boolean isExternallyPowered() { - final Intent intent = mContext.registerReceiver(null, new IntentFilter( - Intent.ACTION_BATTERY_CHANGED)); - final int pluggedState = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1); - return pluggedState == BatteryManager.BATTERY_PLUGGED_AC - || pluggedState == BatteryManager.BATTERY_PLUGGED_USB; - } - - private boolean hasWifiConnection() { - final ConnectivityManager manager = - (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); - final NetworkInfo wifiInfo = manager.getNetworkInfo(ConnectivityManager.TYPE_WIFI); - return wifiInfo.isConnected(); - } - - class UploadRunnable implements Runnable { - private final Callback mCallback; - private final boolean mForceUpload; - - public UploadRunnable(final Callback callback, final boolean forceUpload) { - mCallback = callback; - mForceUpload = forceUpload; - } - - @Override - public void run() { - doUpload(); - } - - private void doUpload() { - if (!mForceUpload && (!isExternallyPowered() || !hasWifiConnection())) { - return; - } - if (mFilesDir == null) { - return; - } - final File[] files = mFilesDir.listFiles(new FileFilter() { - @Override - public boolean accept(File pathname) { - return pathname.getName().startsWith(ResearchLogger.FILENAME_PREFIX) - && !pathname.canWrite(); - } - }); - boolean success = true; - if (files.length == 0) { - success = false; - } - for (final File file : files) { - if (!uploadFile(file)) { - success = false; - } - } - if (mCallback != null) { - mCallback.onUploadCompleted(success); - } - } - - private boolean uploadFile(File file) { - Log.d(TAG, "attempting upload of " + file.getAbsolutePath()); - boolean success = false; - final int contentLength = (int) file.length(); - HttpURLConnection connection = null; - InputStream fileIs = null; - try { - fileIs = new FileInputStream(file); - connection = (HttpURLConnection) mUrl.openConnection(); - connection.setRequestMethod("PUT"); - connection.setDoOutput(true); - connection.setFixedLengthStreamingMode(contentLength); - final OutputStream os = connection.getOutputStream(); - final byte[] buf = new byte[BUF_SIZE]; - int numBytesRead; - while ((numBytesRead = fileIs.read(buf)) != -1) { - os.write(buf, 0, numBytesRead); - } - if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) { - Log.d(TAG, "upload failed: " + connection.getResponseCode()); - InputStream netIs = connection.getInputStream(); - BufferedReader reader = new BufferedReader(new InputStreamReader(netIs)); - String line; - while ((line = reader.readLine()) != null) { - Log.d(TAG, "| " + reader.readLine()); - } - reader.close(); - return success; - } - file.delete(); - success = true; - Log.d(TAG, "upload successful"); - } catch (Exception e) { - e.printStackTrace(); - } finally { - if (fileIs != null) { - try { - fileIs.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - if (connection != null) { - connection.disconnect(); - } - } - return success; - } - } -} diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java index cf6f31a0a..5c2487195 100644 --- a/java/src/com/android/inputmethod/research/ResearchLogger.java +++ b/java/src/com/android/inputmethod/research/ResearchLogger.java @@ -18,11 +18,14 @@ package com.android.inputmethod.research; import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET; +import android.app.AlarmManager; import android.app.AlertDialog; import android.app.Dialog; +import android.app.PendingIntent; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnCancelListener; +import android.content.Intent; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.content.pm.PackageInfo; @@ -32,28 +35,30 @@ import android.graphics.Color; import android.graphics.Paint; import android.graphics.Paint.Style; import android.inputmethodservice.InputMethodService; +import android.net.Uri; import android.os.Build; import android.os.IBinder; +import android.os.SystemClock; import android.text.TextUtils; import android.text.format.DateUtils; import android.util.Log; +import android.view.KeyEvent; import android.view.MotionEvent; -import android.view.View; -import android.view.View.OnClickListener; import android.view.Window; import android.view.WindowManager; import android.view.inputmethod.CompletionInfo; +import android.view.inputmethod.CorrectionInfo; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; -import android.widget.Button; import android.widget.Toast; import com.android.inputmethod.keyboard.Key; import com.android.inputmethod.keyboard.Keyboard; import com.android.inputmethod.keyboard.KeyboardId; -import com.android.inputmethod.keyboard.KeyboardSwitcher; import com.android.inputmethod.keyboard.KeyboardView; import com.android.inputmethod.keyboard.MainKeyboardView; +import com.android.inputmethod.latin.CollectionUtils; +import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.Dictionary; import com.android.inputmethod.latin.LatinIME; import com.android.inputmethod.latin.R; @@ -64,11 +69,8 @@ import com.android.inputmethod.latin.SuggestedWords; import com.android.inputmethod.latin.define.ProductionFlag; import java.io.File; -import java.io.IOException; import java.text.SimpleDateFormat; -import java.util.ArrayList; import java.util.Date; -import java.util.List; import java.util.Locale; import java.util.UUID; @@ -94,24 +96,26 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang new SimpleDateFormat("yyyyMMddHHmmssS", Locale.US); private static final boolean IS_SHOWING_INDICATOR = true; private static final boolean IS_SHOWING_INDICATOR_CLEARLY = false; + public static final int FEEDBACK_WORD_BUFFER_SIZE = 5; // constants related to specific log points private static final String WHITESPACE_SEPARATORS = " \t\n\r"; private static final int MAX_INPUTVIEW_LENGTH_TO_CAPTURE = 8192; // must be >=1 private static final String PREF_RESEARCH_LOGGER_UUID_STRING = "pref_research_logger_uuid"; - private static final int ABORT_TIMEOUT_IN_MS = 10 * 1000; // timeout to notify user private static final ResearchLogger sInstance = new ResearchLogger(); // to write to a different filename, e.g., for testing, set mFile before calling start() /* package */ File mFilesDir; /* package */ String mUUIDString; /* package */ ResearchLog mMainResearchLog; - // The mIntentionalResearchLog records all events for the session, private or not (excepting + // mFeedbackLog records all events for the session, private or not (excepting // passwords). It is written to permanent storage only if the user explicitly commands // the system to do so. - /* package */ ResearchLog mIntentionalResearchLog; - // LogUnits are queued here and released only when the user requests the intentional log. - private List<LogUnit> mIntentionalResearchLogQueue = new ArrayList<LogUnit>(); + // LogUnits are queued in the LogBuffers and published to the ResearchLogs when words are + // complete. + /* package */ ResearchLog mFeedbackLog; + /* package */ MainLogBuffer mMainLogBuffer; + /* package */ LogBuffer mFeedbackLogBuffer; private boolean mIsPasswordView = false; private boolean mIsLoggingSuspended = false; @@ -133,20 +137,24 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang // used to check whether words are not unique private Suggest mSuggest; private Dictionary mDictionary; - private KeyboardSwitcher mKeyboardSwitcher; + private MainKeyboardView mMainKeyboardView; private InputMethodService mInputMethodService; + private final Statistics mStatistics; - private ResearchLogUploader mResearchLogUploader; + private Intent mUploadIntent; + private PendingIntent mUploadPendingIntent; + + private LogUnit mCurrentLogUnit = new LogUnit(); private ResearchLogger() { + mStatistics = Statistics.getInstance(); } public static ResearchLogger getInstance() { return sInstance; } - public void init(final InputMethodService ims, final SharedPreferences prefs, - KeyboardSwitcher keyboardSwitcher) { + public void init(final InputMethodService ims, final SharedPreferences prefs) { assert ims != null; if (ims == null) { Log.w(TAG, "IMS is null; logging is off"); @@ -176,11 +184,33 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang e.apply(); } } - mResearchLogUploader = new ResearchLogUploader(ims, mFilesDir); - mResearchLogUploader.start(); - mKeyboardSwitcher = keyboardSwitcher; mInputMethodService = ims; mPrefs = prefs; + mUploadIntent = new Intent(mInputMethodService, UploaderService.class); + mUploadPendingIntent = PendingIntent.getService(mInputMethodService, 0, mUploadIntent, 0); + + if (ProductionFlag.IS_EXPERIMENTAL) { + scheduleUploadingService(mInputMethodService); + } + } + + /** + * Arrange for the UploaderService to be run on a regular basis. + * + * Any existing scheduled invocation of UploaderService is removed and rescheduled. This may + * cause problems if this method is called often and frequent updates are required, but since + * the user will likely be sleeping at some point, if the interval is less that the expected + * sleep duration and this method is not called during that time, the service should be invoked + * at some point. + */ + public static void scheduleUploadingService(Context context) { + final Intent intent = new Intent(context, UploaderService.class); + final PendingIntent pendingIntent = PendingIntent.getService(context, 0, intent, 0); + final AlarmManager manager = + (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + manager.cancel(pendingIntent); + manager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, + UploaderService.RUN_INTERVAL, UploaderService.RUN_INTERVAL, pendingIntent); } private void cleanupLoggingDir(final File dir, final long time) { @@ -192,10 +222,15 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } } - public void mainKeyboardView_onAttachedToWindow() { + public void mainKeyboardView_onAttachedToWindow(final MainKeyboardView mainKeyboardView) { + mMainKeyboardView = mainKeyboardView; maybeShowSplashScreen(); } + public void mainKeyboardView_onDetachedFromWindow() { + mMainKeyboardView = null; + } + private boolean hasSeenSplash() { return mPrefs.getBoolean(PREF_RESEARCH_HAS_SEEN_SPLASH, false); } @@ -209,54 +244,71 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang if (mSplashDialog != null && mSplashDialog.isShowing()) { return; } - final IBinder windowToken = mKeyboardSwitcher.getMainKeyboardView().getWindowToken(); + final IBinder windowToken = mMainKeyboardView != null + ? mMainKeyboardView.getWindowToken() : null; if (windowToken == null) { return; } - mSplashDialog = new Dialog(mInputMethodService, android.R.style.Theme_Holo_Dialog); - mSplashDialog.requestWindowFeature(Window.FEATURE_NO_TITLE); - mSplashDialog.setContentView(R.layout.research_splash); - mSplashDialog.setCancelable(true); + final AlertDialog.Builder builder = new AlertDialog.Builder(mInputMethodService) + .setTitle(R.string.research_splash_title) + .setMessage(R.string.research_splash_content) + .setPositiveButton(android.R.string.yes, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + onUserLoggingConsent(); + mSplashDialog.dismiss(); + } + }) + .setNegativeButton(android.R.string.no, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + final String packageName = mInputMethodService.getPackageName(); + final Uri packageUri = Uri.parse("package:" + packageName); + final Intent intent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE, + packageUri); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + mInputMethodService.startActivity(intent); + } + }) + .setCancelable(true) + .setOnCancelListener( + new OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + mInputMethodService.requestHideSelf(0); + } + }); + mSplashDialog = builder.create(); final Window w = mSplashDialog.getWindow(); final WindowManager.LayoutParams lp = w.getAttributes(); lp.token = windowToken; lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG; w.setAttributes(lp); w.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); - mSplashDialog.setOnCancelListener(new OnCancelListener() { - @Override - public void onCancel(DialogInterface dialog) { - mInputMethodService.requestHideSelf(0); - } - }); - final Button doNotLogButton = (Button) mSplashDialog.findViewById( - R.id.research_do_not_log_button); - doNotLogButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - onUserLoggingElection(false); - mSplashDialog.dismiss(); - } - }); - final Button doLogButton = (Button) mSplashDialog.findViewById(R.id.research_do_log_button); - doLogButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - onUserLoggingElection(true); - mSplashDialog.dismiss(); - } - }); mSplashDialog.show(); } - public void onUserLoggingElection(final boolean enableLogging) { - setLoggingAllowed(enableLogging); + public void onUserLoggingConsent() { + setLoggingAllowed(true); if (mPrefs == null) { return; } final Editor e = mPrefs.edit(); e.putBoolean(PREF_RESEARCH_HAS_SEEN_SPLASH, true); e.apply(); + restart(); + } + + private void setLoggingAllowed(boolean enableLogging) { + if (mPrefs == null) { + return; + } + Editor e = mPrefs.edit(); + e.putBoolean(PREF_USABILITY_STUDY_MODE, enableLogging); + e.apply(); + sIsLogging = enableLogging; } private File createLogFile(File filesDir) { @@ -268,10 +320,35 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang return new File(filesDir, sb.toString()); } + private void checkForEmptyEditor() { + if (mInputMethodService == null) { + return; + } + final InputConnection ic = mInputMethodService.getCurrentInputConnection(); + if (ic == null) { + return; + } + final CharSequence textBefore = ic.getTextBeforeCursor(1, 0); + if (!TextUtils.isEmpty(textBefore)) { + mStatistics.setIsEmptyUponStarting(false); + return; + } + final CharSequence textAfter = ic.getTextAfterCursor(1, 0); + if (!TextUtils.isEmpty(textAfter)) { + mStatistics.setIsEmptyUponStarting(false); + return; + } + if (textBefore != null && textAfter != null) { + mStatistics.setIsEmptyUponStarting(true); + } + } + private void start() { maybeShowSplashScreen(); updateSuspendedState(); requestIndicatorRedraw(); + mStatistics.reset(); + checkForEmptyEditor(); if (!isAllowedToLog()) { // Log.w(TAG, "not in usability mode; not logging"); return; @@ -280,73 +357,58 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang Log.w(TAG, "IME storage directory does not exist. Cannot start logging."); return; } - try { - if (mMainResearchLog == null || !mMainResearchLog.isAlive()) { - mMainResearchLog = new ResearchLog(createLogFile(mFilesDir)); - } - mMainResearchLog.start(); - if (mIntentionalResearchLog == null || !mIntentionalResearchLog.isAlive()) { - mIntentionalResearchLog = new ResearchLog(createLogFile(mFilesDir)); - } - mIntentionalResearchLog.start(); - } catch (IOException e) { - Log.w(TAG, "Could not start ResearchLogger."); + if (mMainLogBuffer == null) { + mMainResearchLog = new ResearchLog(createLogFile(mFilesDir)); + mMainLogBuffer = new MainLogBuffer(mMainResearchLog); + mMainLogBuffer.setSuggest(mSuggest); + } + if (mFeedbackLogBuffer == null) { + mFeedbackLog = new ResearchLog(createLogFile(mFilesDir)); + // LogBuffer is one more than FEEDBACK_WORD_BUFFER_SIZE, because it must also hold + // the feedback LogUnit itself. + mFeedbackLogBuffer = new LogBuffer(FEEDBACK_WORD_BUFFER_SIZE + 1); } } /* package */ void stop() { - if (mMainResearchLog != null) { - mMainResearchLog.stop(); - } - if (mIntentionalResearchLog != null) { - mIntentionalResearchLog.stop(); - } - } + logStatistics(); + commitCurrentLogUnit(); - private void setLoggingAllowed(boolean enableLogging) { - if (mPrefs == null) { - return; + if (mMainLogBuffer != null) { + publishLogBuffer(mMainLogBuffer, mMainResearchLog, false /* isIncludingPrivateData */); + mMainResearchLog.close(); + mMainLogBuffer = null; + } + if (mFeedbackLogBuffer != null) { + mFeedbackLog.close(); + mFeedbackLogBuffer = null; } - Editor e = mPrefs.edit(); - e.putBoolean(PREF_USABILITY_STUDY_MODE, enableLogging); - e.apply(); - sIsLogging = enableLogging; } public boolean abort() { boolean didAbortMainLog = false; - if (mMainResearchLog != null) { - mMainResearchLog.abort(); + if (mMainLogBuffer != null) { + mMainLogBuffer.clear(); try { - mMainResearchLog.waitUntilStopped(ABORT_TIMEOUT_IN_MS); + didAbortMainLog = mMainResearchLog.blockingAbort(); } catch (InterruptedException e) { - // interrupted early. carry on. + // Don't know whether this succeeded or not. We assume not; this is reported + // to the caller. } - if (mMainResearchLog.isAbortSuccessful()) { - didAbortMainLog = true; - } - mMainResearchLog = null; + mMainLogBuffer = null; } - boolean didAbortIntentionalLog = false; - if (mIntentionalResearchLog != null) { - mIntentionalResearchLog.abort(); + boolean didAbortFeedbackLog = false; + if (mFeedbackLogBuffer != null) { + mFeedbackLogBuffer.clear(); try { - mIntentionalResearchLog.waitUntilStopped(ABORT_TIMEOUT_IN_MS); + didAbortFeedbackLog = mFeedbackLog.blockingAbort(); } catch (InterruptedException e) { - // interrupted early. carry on. - } - if (mIntentionalResearchLog.isAbortSuccessful()) { - didAbortIntentionalLog = true; + // Don't know whether this succeeded or not. We assume not; this is reported + // to the caller. } - mIntentionalResearchLog = null; - } - return didAbortMainLog && didAbortIntentionalLog; - } - - /* package */ void flush() { - if (mMainResearchLog != null) { - mMainResearchLog.flush(); + mFeedbackLogBuffer = null; } + return didAbortMainLog && didAbortFeedbackLog; } private void restart() { @@ -387,14 +449,22 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang abort(); } requestIndicatorRedraw(); + mPrefs = prefs; + prefsChanged(prefs); } - public void presentResearchDialog(final LatinIME latinIME) { + public void onResearchKeySelected(final LatinIME latinIME) { if (mInFeedbackDialog) { Toast.makeText(latinIME, R.string.research_please_exit_feedback_form, Toast.LENGTH_LONG).show(); return; } + 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[] { @@ -411,28 +481,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang presentFeedbackDialog(latinIME); break; case 1: - if (showEnable) { - if (!sIsLogging) { - setLoggingAllowed(true); - } - resumeLogging(); - Toast.makeText(latinIME, - R.string.research_notify_session_logging_enabled, - Toast.LENGTH_LONG).show(); - } else { - Toast toast = Toast.makeText(latinIME, - R.string.research_notify_session_log_deleting, - Toast.LENGTH_LONG); - toast.show(); - boolean isLogDeleted = abort(); - final long currentTime = System.currentTimeMillis(); - final long resumeTime = currentTime + 1000 * 60 * - SUSPEND_DURATION_IN_MINUTES; - suspendLoggingUntil(resumeTime); - toast.cancel(); - Toast.makeText(latinIME, R.string.research_notify_logging_suspended, - Toast.LENGTH_LONG).show(); - } + enableOrDisable(showEnable, latinIME); break; } } @@ -443,6 +492,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang .setTitle(title); latinIME.showOptionDialog(builder.create()); } + */ private boolean mInFeedbackDialog = false; public void presentFeedbackDialog(LatinIME latinIME) { @@ -450,79 +500,73 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang latinIME.launchKeyboardedDialogActivity(FeedbackActivity.class); } - private ResearchLog mFeedbackLog; - private List<LogUnit> mFeedbackQueue; - private ResearchLog mSavedMainResearchLog; - private ResearchLog mSavedIntentionalResearchLog; - private List<LogUnit> mSavedIntentionalResearchLogQueue; - - private void saveLogsForFeedback() { - mFeedbackLog = mIntentionalResearchLog; - if (mIntentionalResearchLogQueue != null) { - mFeedbackQueue = new ArrayList<LogUnit>(mIntentionalResearchLogQueue); + // TODO: currently unreachable. Remove after being sure enable/disable is + // not needed. + /* + public void enableOrDisable(final boolean showEnable, final LatinIME latinIME) { + if (showEnable) { + if (!sIsLogging) { + setLoggingAllowed(true); + } + resumeLogging(); + Toast.makeText(latinIME, + R.string.research_notify_session_logging_enabled, + Toast.LENGTH_LONG).show(); } else { - mFeedbackQueue = null; + Toast toast = Toast.makeText(latinIME, + R.string.research_notify_session_log_deleting, + Toast.LENGTH_LONG); + toast.show(); + boolean isLogDeleted = abort(); + final long currentTime = System.currentTimeMillis(); + final long resumeTime = currentTime + 1000 * 60 * + SUSPEND_DURATION_IN_MINUTES; + suspendLoggingUntil(resumeTime); + toast.cancel(); + Toast.makeText(latinIME, R.string.research_notify_logging_suspended, + Toast.LENGTH_LONG).show(); } - mSavedMainResearchLog = mMainResearchLog; - mSavedIntentionalResearchLog = mIntentionalResearchLog; - mSavedIntentionalResearchLogQueue = mIntentionalResearchLogQueue; - - mMainResearchLog = null; - mIntentionalResearchLog = null; - mIntentionalResearchLogQueue = new ArrayList<LogUnit>(); } + */ - private static final int LOG_DRAIN_TIMEOUT_IN_MS = 1000 * 5; + private static final String[] EVENTKEYS_FEEDBACK = { + "UserTimestamp", "contents" + }; public void sendFeedback(final String feedbackContents, final boolean includeHistory) { - if (includeHistory && mFeedbackLog != null) { - try { - LogUnit headerLogUnit = new LogUnit(); - headerLogUnit.addLogAtom(EVENTKEYS_INTENTIONAL_LOG, EVENTKEYS_NULLVALUES, false); - mFeedbackLog.publishAllEvents(headerLogUnit); - for (LogUnit logUnit : mFeedbackQueue) { - mFeedbackLog.publishAllEvents(logUnit); - } - userFeedback(mFeedbackLog, feedbackContents); - mFeedbackLog.stop(); - try { - mFeedbackLog.waitUntilStopped(LOG_DRAIN_TIMEOUT_IN_MS); - } catch (InterruptedException e) { - e.printStackTrace(); - } - mIntentionalResearchLog = new ResearchLog(createLogFile(mFilesDir)); - mIntentionalResearchLog.start(); - } catch (IOException e) { - e.printStackTrace(); - } finally { - mIntentionalResearchLogQueue.clear(); - } - mResearchLogUploader.uploadNow(null); + if (mFeedbackLogBuffer == null) { + return; + } + if (includeHistory) { + commitCurrentLogUnit(); } else { - // create a separate ResearchLog just for feedback - final ResearchLog feedbackLog = new ResearchLog(createLogFile(mFilesDir)); - try { - feedbackLog.start(); - userFeedback(feedbackLog, feedbackContents); - feedbackLog.stop(); - feedbackLog.waitUntilStopped(LOG_DRAIN_TIMEOUT_IN_MS); - mResearchLogUploader.uploadNow(null); - } catch (IOException e) { - e.printStackTrace(); - } catch (InterruptedException e) { - e.printStackTrace(); - } + mFeedbackLogBuffer.clear(); } + final LogUnit feedbackLogUnit = new LogUnit(); + final Object[] values = { + feedbackContents + }; + feedbackLogUnit.addLogStatement(EVENTKEYS_FEEDBACK, values, + false /* isPotentiallyPrivate */); + mFeedbackLogBuffer.shiftIn(feedbackLogUnit); + publishLogBuffer(mFeedbackLogBuffer, mFeedbackLog, true /* isIncludingPrivateData */); + mFeedbackLog.close(); + uploadNow(); + mFeedbackLog = new ResearchLog(createLogFile(mFilesDir)); + } + + public void uploadNow() { + mInputMethodService.startService(mUploadIntent); } public void onLeavingSendFeedbackDialog() { mInFeedbackDialog = false; - mMainResearchLog = mSavedMainResearchLog; - mIntentionalResearchLog = mSavedIntentionalResearchLog; - mIntentionalResearchLogQueue = mSavedIntentionalResearchLogQueue; } public void initSuggest(Suggest suggest) { mSuggest = suggest; + if (mMainLogBuffer != null) { + mMainLogBuffer.setSuggest(mSuggest); + } } private void setIsPasswordView(boolean isPasswordView) { @@ -530,21 +574,17 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } private boolean isAllowedToLog() { - return !mIsPasswordView && !mIsLoggingSuspended && sIsLogging; + return !mIsPasswordView && !mIsLoggingSuspended && sIsLogging && !mInFeedbackDialog; } public void requestIndicatorRedraw() { if (!IS_SHOWING_INDICATOR) { return; } - if (mKeyboardSwitcher == null) { - return; - } - final KeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); - if (mainKeyboardView == null) { + if (mMainKeyboardView == null) { return; } - mainKeyboardView.invalidateAllKeys(); + mMainKeyboardView.invalidateAllKeys(); } @@ -577,13 +617,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } } - private static final String CURRENT_TIME_KEY = "_ct"; - private static final String UPTIME_KEY = "_ut"; - private static final String EVENT_TYPE_KEY = "_ty"; private static final Object[] EVENTKEYS_NULLVALUES = {}; - private LogUnit mCurrentLogUnit = new LogUnit(); - /** * Buffer a research log event, flagging it as privacy-sensitive. * @@ -599,10 +634,14 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang final Object[] values) { assert values.length + 1 == keys.length; if (isAllowedToLog()) { - mCurrentLogUnit.addLogAtom(keys, values, true); + mCurrentLogUnit.addLogStatement(keys, values, true /* isPotentiallyPrivate */); } } + private void setCurrentLogUnitContainsDigitFlag() { + mCurrentLogUnit.setContainsDigit(); + } + /** * Buffer a research log event, flaggint it as not privacy-sensitive. * @@ -618,139 +657,54 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang private synchronized void enqueueEvent(final String[] keys, final Object[] values) { assert values.length + 1 == keys.length; if (isAllowedToLog()) { - mCurrentLogUnit.addLogAtom(keys, values, false); + mCurrentLogUnit.addLogStatement(keys, values, false /* isPotentiallyPrivate */); } } - // Used to track how often words are logged. Too-frequent logging can leak - // semantics, disclosing private data. - /* package for test */ static class LoggingFrequencyState { - private static final int DEFAULT_WORD_LOG_FREQUENCY = 10; - private int mWordsRemainingToSkip; - private final int mFrequency; - - /** - * Tracks how often words may be uploaded. - * - * @param frequency 1=Every word, 2=Every other word, etc. - */ - public LoggingFrequencyState(int frequency) { - mFrequency = frequency; - mWordsRemainingToSkip = mFrequency; - } - - public void onWordLogged() { - mWordsRemainingToSkip = mFrequency; - } - - public void onWordNotLogged() { - if (mWordsRemainingToSkip > 1) { - mWordsRemainingToSkip--; + /* package for test */ void commitCurrentLogUnit() { + if (!mCurrentLogUnit.isEmpty()) { + if (mMainLogBuffer != null) { + mMainLogBuffer.shiftIn(mCurrentLogUnit); + if (mMainLogBuffer.isSafeToLog() && mMainResearchLog != null) { + publishLogBuffer(mMainLogBuffer, mMainResearchLog, + true /* isIncludingPrivateData */); + mMainLogBuffer.resetWordCounter(); + } } + if (mFeedbackLogBuffer != null) { + mFeedbackLogBuffer.shiftIn(mCurrentLogUnit); + } + mCurrentLogUnit = new LogUnit(); + Log.d(TAG, "commitCurrentLogUnit"); } + } - public boolean isSafeToLog() { - return mWordsRemainingToSkip <= 1; + /* package for test */ void publishLogBuffer(final LogBuffer logBuffer, + final ResearchLog researchLog, final boolean isIncludingPrivateData) { + LogUnit logUnit; + while ((logUnit = logBuffer.shiftOut()) != null) { + researchLog.publish(logUnit, isIncludingPrivateData); } } - /* package for test */ LoggingFrequencyState mLoggingFrequencyState = - new LoggingFrequencyState(LoggingFrequencyState.DEFAULT_WORD_LOG_FREQUENCY); - - /* package for test */ boolean isPrivacyThreat(String word) { - // Current checks: - // - Word not in dictionary - // - Word contains numbers - // - Privacy-safe word not logged recently - if (TextUtils.isEmpty(word)) { - return false; - } - if (!mLoggingFrequencyState.isSafeToLog()) { - return true; - } + private boolean hasOnlyLetters(final String word) { final int length = word.length(); - boolean hasLetter = false; for (int i = 0; i < length; i = word.offsetByCodePoints(i, 1)) { - final int codePoint = Character.codePointAt(word, i); - if (Character.isDigit(codePoint)) { - return true; - } - if (Character.isLetter(codePoint)) { - hasLetter = true; - break; // Word may contain digits, but will only be allowed if in the dictionary. - } - } - if (hasLetter) { - if (mDictionary == null && mSuggest != null && mSuggest.hasMainDictionary()) { - mDictionary = mSuggest.getMainDictionary(); - } - if (mDictionary == null) { - // Can't access dictionary. Assume privacy threat. - return true; + final int codePoint = word.codePointAt(i); + if (!Character.isLetter(codePoint)) { + return false; } - return !(mDictionary.isValidWord(word)); - } - // No letters, no numbers. Punctuation, space, or something else. - return false; - } - - private void onWordComplete(String word) { - if (isPrivacyThreat(word)) { - publishLogUnit(mCurrentLogUnit, true); - mLoggingFrequencyState.onWordNotLogged(); - } else { - publishLogUnit(mCurrentLogUnit, false); - mLoggingFrequencyState.onWordLogged(); } - mCurrentLogUnit = new LogUnit(); + return true; } - private void publishLogUnit(LogUnit logUnit, boolean isPrivacySensitive) { - if (!isAllowedToLog()) { - return; - } - if (mMainResearchLog == null) { - return; - } - if (isPrivacySensitive) { - mMainResearchLog.publishPublicEvents(logUnit); - } else { - mMainResearchLog.publishAllEvents(logUnit); - } - mIntentionalResearchLogQueue.add(logUnit); - } - - /* package */ void publishCurrentLogUnit(ResearchLog researchLog, boolean isPrivacySensitive) { - publishLogUnit(mCurrentLogUnit, isPrivacySensitive); - } - - static class LogUnit { - private final List<String[]> mKeysList = new ArrayList<String[]>(); - private final List<Object[]> mValuesList = new ArrayList<Object[]>(); - private final List<Boolean> mIsPotentiallyPrivate = new ArrayList<Boolean>(); - - private void addLogAtom(final String[] keys, final Object[] values, - final Boolean isPotentiallyPrivate) { - mKeysList.add(keys); - mValuesList.add(values); - mIsPotentiallyPrivate.add(isPotentiallyPrivate); - } - - public void publishPublicEventsTo(ResearchLog researchLog) { - final int size = mKeysList.size(); - for (int i = 0; i < size; i++) { - if (!mIsPotentiallyPrivate.get(i)) { - researchLog.outputEvent(mKeysList.get(i), mValuesList.get(i)); - } - } - } - - public void publishAllEventsTo(ResearchLog researchLog) { - final int size = mKeysList.size(); - for (int i = 0; i < size; i++) { - researchLog.outputEvent(mKeysList.get(i), mValuesList.get(i)); - } + private void onWordComplete(final String word) { + Log.d(TAG, "onWordComplete: " + word); + if (word != null && word.length() > 0 && hasOnlyLetters(word)) { + mCurrentLogUnit.setWord(word); + mStatistics.recordWordEntered(); } + commitCurrentLogUnit(); } private static int scrubDigitFromCodePoint(int codePoint) { @@ -803,12 +757,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang return WORD_REPLACEMENT_STRING; } - // Special methods related to startup, shutdown, logging itself - - private static final String[] EVENTKEYS_INTENTIONAL_LOG = { - "IntentionalLog" - }; - private static final String[] EVENTKEYS_LATINIME_ONSTARTINPUTVIEWINTERNAL = { "LatinIMEOnStartInputViewInternal", "uuid", "packageName", "inputType", "imeOptions", "fieldId", "display", "model", "prefs", "versionCode", "versionName", "outputFormatVersion" @@ -816,9 +764,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang public static void latinIME_onStartInputViewInternal(final EditorInfo editorInfo, final SharedPreferences prefs) { final ResearchLogger researchLogger = getInstance(); - if (researchLogger.mInFeedbackDialog) { - researchLogger.saveLogsForFeedback(); - } researchLogger.start(); if (editorInfo != null) { final Context context = researchLogger.mInputMethodService; @@ -846,32 +791,19 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang stop(); } - private static final String[] EVENTKEYS_LATINIME_COMMITTEXT = { - "LatinIMECommitText", "typedWord" - }; - - public static void latinIME_commitText(final CharSequence typedWord) { - final String scrubbedWord = scrubDigitsFromString(typedWord.toString()); - final Object[] values = { - scrubbedWord - }; - final ResearchLogger researchLogger = getInstance(); - researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_COMMITTEXT, values); - researchLogger.onWordComplete(scrubbedWord); - } - private static final String[] EVENTKEYS_USER_FEEDBACK = { "UserFeedback", "FeedbackContents" }; - private void userFeedback(ResearchLog researchLog, String feedbackContents) { - // this method is special; it directs the feedbackContents to a particular researchLog - final LogUnit logUnit = new LogUnit(); + private static final String[] EVENTKEYS_PREFS_CHANGED = { + "PrefsChanged", "prefs" + }; + public static void prefsChanged(final SharedPreferences prefs) { + final ResearchLogger researchLogger = getInstance(); final Object[] values = { - feedbackContents + prefs }; - logUnit.addLogAtom(EVENTKEYS_USER_FEEDBACK, values, false); - researchLog.publishAllEvents(logUnit); + researchLogger.enqueueEvent(EVENTKEYS_PREFS_CHANGED, values); } // Regular logging methods @@ -908,51 +840,16 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang "LatinIMEOnCodeInput", "code", "x", "y" }; public static void latinIME_onCodeInput(final int code, final int x, final int y) { - final Object[] values = { - Keyboard.printableCode(scrubDigitFromCodePoint(code)), x, y - }; - getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_ONCODEINPUT, values); - } - - private static final String[] EVENTKEYS_CORRECTION = { - "LogCorrection", "subgroup", "before", "after", "position" - }; - public static void logCorrection(final String subgroup, final String before, final String after, - final int position) { - final Object[] values = { - subgroup, scrubDigitsFromString(before), scrubDigitsFromString(after), position - }; - getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_CORRECTION, values); - } - - private static final String[] EVENTKEYS_LATINIME_COMMITCURRENTAUTOCORRECTION = { - "LatinIMECommitCurrentAutoCorrection", "typedWord", "autoCorrection" - }; - public static void latinIME_commitCurrentAutoCorrection(final String typedWord, - final String autoCorrection) { - final Object[] values = { - scrubDigitsFromString(typedWord), scrubDigitsFromString(autoCorrection) - }; + final long time = SystemClock.uptimeMillis(); final ResearchLogger researchLogger = getInstance(); - researchLogger.enqueuePotentiallyPrivateEvent( - EVENTKEYS_LATINIME_COMMITCURRENTAUTOCORRECTION, values); - } - - private static final String[] EVENTKEYS_LATINIME_DELETESURROUNDINGTEXT = { - "LatinIMEDeleteSurroundingText", "length" - }; - public static void latinIME_deleteSurroundingText(final int length) { final Object[] values = { - length + Keyboard.printableCode(scrubDigitFromCodePoint(code)), x, y }; - getInstance().enqueueEvent(EVENTKEYS_LATINIME_DELETESURROUNDINGTEXT, values); - } - - private static final String[] EVENTKEYS_LATINIME_DOUBLESPACEAUTOPERIOD = { - "LatinIMEDoubleSpaceAutoPeriod" - }; - public static void latinIME_doubleSpaceAutoPeriod() { - getInstance().enqueueEvent(EVENTKEYS_LATINIME_DOUBLESPACEAUTOPERIOD, EVENTKEYS_NULLVALUES); + researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_ONCODEINPUT, values); + if (Character.isDigit(code)) { + researchLogger.setCurrentLogUnitContainsDigitFlag(); + } + researchLogger.mStatistics.recordChar(code, time); } private static final String[] EVENTKEYS_LATINIME_ONDISPLAYCOMPLETIONS = { @@ -979,6 +876,10 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang public static void latinIME_onWindowHidden(final int savedSelectionStart, final int savedSelectionEnd, final InputConnection ic) { if (ic != null) { + // Capture the TextView contents. This will trigger onUpdateSelection(), so we + // set sLatinIMEExpectingUpdateSelection so that when onUpdateSelection() is called, + // it can tell that it was generated by the logging code, and not by the user, and + // therefore keep user-visible state as is. ic.beginBatchEdit(); ic.performContextMenuAction(android.R.id.selectAll); CharSequence charSequence = ic.getSelectedText(0); @@ -1013,9 +914,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } final ResearchLogger researchLogger = getInstance(); researchLogger.enqueueEvent(EVENTKEYS_LATINIME_ONWINDOWHIDDEN, values); - // Play it safe. Remove privacy-sensitive events. - researchLogger.publishLogUnit(researchLogger.mCurrentLogUnit, true); - researchLogger.mCurrentLogUnit = new LogUnit(); + researchLogger.commitCurrentLogUnit(); getInstance().stop(); } } @@ -1048,37 +947,15 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_ONUPDATESELECTION, values); } - private static final String[] EVENTKEYS_LATINIME_PERFORMEDITORACTION = { - "LatinIMEPerformEditorAction", "imeActionNext" - }; - public static void latinIME_performEditorAction(final int imeActionNext) { - final Object[] values = { - imeActionNext - }; - getInstance().enqueueEvent(EVENTKEYS_LATINIME_PERFORMEDITORACTION, values); - } - - private static final String[] EVENTKEYS_LATINIME_PICKAPPLICATIONSPECIFIEDCOMPLETION = { - "LatinIMEPickApplicationSpecifiedCompletion", "index", "text", "x", "y" - }; - public static void latinIME_pickApplicationSpecifiedCompletion(final int index, - final CharSequence cs, int x, int y) { - final Object[] values = { - index, cs, x, y - }; - final ResearchLogger researchLogger = getInstance(); - researchLogger.enqueuePotentiallyPrivateEvent( - EVENTKEYS_LATINIME_PICKAPPLICATIONSPECIFIEDCOMPLETION, values); - } - private static final String[] EVENTKEYS_LATINIME_PICKSUGGESTIONMANUALLY = { "LatinIMEPickSuggestionManually", "replacedWord", "index", "suggestion", "x", "y" }; public static void latinIME_pickSuggestionManually(final String replacedWord, - final int index, CharSequence suggestion, int x, int y) { + final int index, CharSequence suggestion) { final Object[] values = { - scrubDigitsFromString(replacedWord), index, suggestion == null ? null : - scrubDigitsFromString(suggestion.toString()), x, y + scrubDigitsFromString(replacedWord), index, + (suggestion == null ? null : scrubDigitsFromString(suggestion.toString())), + Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE }; final ResearchLogger researchLogger = getInstance(); researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_PICKSUGGESTIONMANUALLY, @@ -1089,28 +966,14 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang "LatinIMEPunctuationSuggestion", "index", "suggestion", "x", "y" }; public static void latinIME_punctuationSuggestion(final int index, - final CharSequence suggestion, int x, int y) { + final CharSequence suggestion) { final Object[] values = { - index, suggestion, x, y + index, suggestion, + Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE }; getInstance().enqueueEvent(EVENTKEYS_LATINIME_PUNCTUATIONSUGGESTION, values); } - private static final String[] EVENTKEYS_LATINIME_REVERTDOUBLESPACEWHILEINBATCHEDIT = { - "LatinIMERevertDoubleSpaceWhileInBatchEdit" - }; - public static void latinIME_revertDoubleSpaceWhileInBatchEdit() { - getInstance().enqueueEvent(EVENTKEYS_LATINIME_REVERTDOUBLESPACEWHILEINBATCHEDIT, - EVENTKEYS_NULLVALUES); - } - - private static final String[] EVENTKEYS_LATINIME_REVERTSWAPPUNCTUATION = { - "LatinIMERevertSwapPunctuation" - }; - public static void latinIME_revertSwapPunctuation() { - getInstance().enqueueEvent(EVENTKEYS_LATINIME_REVERTSWAPPUNCTUATION, EVENTKEYS_NULLVALUES); - } - private static final String[] EVENTKEYS_LATINIME_SENDKEYCODEPOINT = { "LatinIMESendKeyCodePoint", "code" }; @@ -1118,15 +981,18 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang final Object[] values = { Keyboard.printableCode(scrubDigitFromCodePoint(code)) }; - getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_SENDKEYCODEPOINT, values); + final ResearchLogger researchLogger = getInstance(); + researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_SENDKEYCODEPOINT, values); + if (Character.isDigit(code)) { + researchLogger.setCurrentLogUnitContainsDigitFlag(); + } } - private static final String[] EVENTKEYS_LATINIME_SWAPSWAPPERANDSPACEWHILEINBATCHEDIT = { - "LatinIMESwapSwapperAndSpaceWhileInBatchEdit" + private static final String[] EVENTKEYS_LATINIME_SWAPSWAPPERANDSPACE = { + "LatinIMESwapSwapperAndSpace" }; - public static void latinIME_swapSwapperAndSpaceWhileInBatchEdit() { - getInstance().enqueueEvent(EVENTKEYS_LATINIME_SWAPSWAPPERANDSPACEWHILEINBATCHEDIT, - EVENTKEYS_NULLVALUES); + public static void latinIME_swapSwapperAndSpace() { + getInstance().enqueueEvent(EVENTKEYS_LATINIME_SWAPSWAPPERANDSPACE, EVENTKEYS_NULLVALUES); } private static final String[] EVENTKEYS_MAINKEYBOARDVIEW_ONLONGPRESS = { @@ -1197,7 +1063,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang final int y, final boolean ignoreModifierKey, final boolean altersCode, final int code) { if (key != null) { - CharSequence outputText = key.mOutputText; + String outputText = key.getOutputText(); final Object[] values = { Keyboard.printableCode(scrubDigitFromCodePoint(code)), outputText == null ? null : scrubDigitsFromString(outputText.toString()), @@ -1245,6 +1111,128 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_POINTERTRACKER_ONMOVEEVENT, values); } + private static final String[] EVENTKEYS_RICHINPUTCONNECTION_COMMITCOMPLETION = { + "RichInputConnectionCommitCompletion", "completionInfo" + }; + public static void richInputConnection_commitCompletion(final CompletionInfo completionInfo) { + final Object[] values = { + completionInfo + }; + final ResearchLogger researchLogger = getInstance(); + researchLogger.enqueuePotentiallyPrivateEvent( + EVENTKEYS_RICHINPUTCONNECTION_COMMITCOMPLETION, values); + } + + // Disabled for privacy-protection reasons. Because this event comes after + // richInputConnection_commitText, which is the event used to separate LogUnits, the + // data in this event can be associated with the next LogUnit, revealing information + // about the current word even if it was supposed to be suppressed. The occurrance of + // autocorrection can be determined by examining the difference between the text strings in + // the last call to richInputConnection_setComposingText before + // richInputConnection_commitText, so it's not a data loss. + // TODO: Figure out how to log this event without loss of privacy. + /* + private static final String[] EVENTKEYS_RICHINPUTCONNECTION_COMMITCORRECTION = { + "RichInputConnectionCommitCorrection", "typedWord", "autoCorrection" + }; + */ + public static void richInputConnection_commitCorrection(CorrectionInfo correctionInfo) { + /* + final String typedWord = correctionInfo.getOldText().toString(); + final String autoCorrection = correctionInfo.getNewText().toString(); + final Object[] values = { + scrubDigitsFromString(typedWord), scrubDigitsFromString(autoCorrection) + }; + final ResearchLogger researchLogger = getInstance(); + researchLogger.enqueuePotentiallyPrivateEvent( + EVENTKEYS_RICHINPUTCONNECTION_COMMITCORRECTION, values); + */ + } + + private static final String[] EVENTKEYS_RICHINPUTCONNECTION_COMMITTEXT = { + "RichInputConnectionCommitText", "typedWord", "newCursorPosition" + }; + public static void richInputConnection_commitText(final CharSequence typedWord, + final int newCursorPosition) { + final String scrubbedWord = scrubDigitsFromString(typedWord.toString()); + final Object[] values = { + scrubbedWord, newCursorPosition + }; + final ResearchLogger researchLogger = getInstance(); + researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_RICHINPUTCONNECTION_COMMITTEXT, + values); + researchLogger.onWordComplete(scrubbedWord); + } + + private static final String[] EVENTKEYS_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT = { + "RichInputConnectionDeleteSurroundingText", "beforeLength", "afterLength" + }; + public static void richInputConnection_deleteSurroundingText(final int beforeLength, + final int afterLength) { + final Object[] values = { + beforeLength, afterLength + }; + getInstance().enqueuePotentiallyPrivateEvent( + EVENTKEYS_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT, values); + } + + private static final String[] EVENTKEYS_RICHINPUTCONNECTION_FINISHCOMPOSINGTEXT = { + "RichInputConnectionFinishComposingText" + }; + public static void richInputConnection_finishComposingText() { + getInstance().enqueueEvent(EVENTKEYS_RICHINPUTCONNECTION_FINISHCOMPOSINGTEXT, + EVENTKEYS_NULLVALUES); + } + + private static final String[] EVENTKEYS_RICHINPUTCONNECTION_PERFORMEDITORACTION = { + "RichInputConnectionPerformEditorAction", "imeActionNext" + }; + public static void richInputConnection_performEditorAction(final int imeActionNext) { + final Object[] values = { + imeActionNext + }; + getInstance().enqueueEvent(EVENTKEYS_RICHINPUTCONNECTION_PERFORMEDITORACTION, values); + } + + private static final String[] EVENTKEYS_RICHINPUTCONNECTION_SENDKEYEVENT = { + "RichInputConnectionSendKeyEvent", "eventTime", "action", "code" + }; + public static void richInputConnection_sendKeyEvent(final KeyEvent keyEvent) { + final Object[] values = { + keyEvent.getEventTime(), + keyEvent.getAction(), + keyEvent.getKeyCode() + }; + getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_RICHINPUTCONNECTION_SENDKEYEVENT, + values); + } + + private static final String[] EVENTKEYS_RICHINPUTCONNECTION_SETCOMPOSINGTEXT = { + "RichInputConnectionSetComposingText", "text", "newCursorPosition" + }; + public static void richInputConnection_setComposingText(final CharSequence text, + final int newCursorPosition) { + if (text == null) { + throw new RuntimeException("setComposingText is null"); + } + final Object[] values = { + text, newCursorPosition + }; + getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_RICHINPUTCONNECTION_SETCOMPOSINGTEXT, + values); + } + + private static final String[] EVENTKEYS_RICHINPUTCONNECTION_SETSELECTION = { + "RichInputConnectionSetSelection", "from", "to" + }; + public static void richInputConnection_setSelection(final int from, final int to) { + final Object[] values = { + from, to + }; + getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_RICHINPUTCONNECTION_SETSELECTION, + values); + } + private static final String[] EVENTKEYS_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT = { "SuddenJumpingTouchEventHandlerOnTouchEvent", "motionEvent" }; @@ -1277,4 +1265,24 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang public void userTimestamp() { getInstance().enqueueEvent(EVENTKEYS_USER_TIMESTAMP, EVENTKEYS_NULLVALUES); } + + private static final String[] EVENTKEYS_STATISTICS = { + "Statistics", "charCount", "letterCount", "numberCount", "spaceCount", "deleteOpsCount", + "wordCount", "isEmptyUponStarting", "isEmptinessStateKnown", "averageTimeBetweenKeys", + "averageTimeBeforeDelete", "averageTimeDuringRepeatedDelete", "averageTimeAfterDelete" + }; + private static void logStatistics() { + final ResearchLogger researchLogger = getInstance(); + final Statistics statistics = researchLogger.mStatistics; + final Object[] values = { + statistics.mCharCount, statistics.mLetterCount, statistics.mNumberCount, + statistics.mSpaceCount, statistics.mDeleteKeyCount, + statistics.mWordCount, statistics.mIsEmptyUponStarting, + statistics.mIsEmptinessStateKnown, statistics.mKeyCounter.getAverageTime(), + statistics.mBeforeDeleteKeyCounter.getAverageTime(), + statistics.mDuringRepeatedDeleteKeysCounter.getAverageTime(), + statistics.mAfterDeleteKeyCounter.getAverageTime() + }; + researchLogger.enqueueEvent(EVENTKEYS_STATISTICS, values); + } } diff --git a/java/src/com/android/inputmethod/research/Statistics.java b/java/src/com/android/inputmethod/research/Statistics.java new file mode 100644 index 000000000..eab465aa2 --- /dev/null +++ b/java/src/com/android/inputmethod/research/Statistics.java @@ -0,0 +1,146 @@ +/* + * 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 com.android.inputmethod.keyboard.Keyboard; + +public class Statistics { + // Number of characters entered during a typing session + int mCharCount; + // Number of letter characters entered during a typing session + int mLetterCount; + // Number of number characters entered + int mNumberCount; + // Number of space characters entered + int mSpaceCount; + // Number of delete operations entered (taps on the backspace key) + int mDeleteKeyCount; + // Number of words entered during a session. + int mWordCount; + // Whether the text field was empty upon editing + boolean mIsEmptyUponStarting; + boolean mIsEmptinessStateKnown; + + // Timers to count average time to enter a key, first press a delete key, + // between delete keys, and then to return typing after a delete key. + final AverageTimeCounter mKeyCounter = new AverageTimeCounter(); + final AverageTimeCounter mBeforeDeleteKeyCounter = new AverageTimeCounter(); + final AverageTimeCounter mDuringRepeatedDeleteKeysCounter = new AverageTimeCounter(); + final AverageTimeCounter mAfterDeleteKeyCounter = new AverageTimeCounter(); + + static class AverageTimeCounter { + int mCount; + int mTotalTime; + + public void reset() { + mCount = 0; + mTotalTime = 0; + } + + public void add(long deltaTime) { + mCount++; + mTotalTime += deltaTime; + } + + public int getAverageTime() { + if (mCount == 0) { + return 0; + } + return mTotalTime / mCount; + } + } + + // To account for the interruptions when the user's attention is directed elsewhere, times + // longer than MIN_TYPING_INTERMISSION are not counted when estimating this statistic. + public static final int MIN_TYPING_INTERMISSION = 2 * 1000; // in milliseconds + public static final int MIN_DELETION_INTERMISSION = 10 * 1000; // in milliseconds + + // The last time that a tap was performed + private long mLastTapTime; + // The type of the last keypress (delete key or not) + boolean mIsLastKeyDeleteKey; + + private static final Statistics sInstance = new Statistics(); + + public static Statistics getInstance() { + return sInstance; + } + + private Statistics() { + reset(); + } + + public void reset() { + mCharCount = 0; + mLetterCount = 0; + mNumberCount = 0; + mSpaceCount = 0; + mDeleteKeyCount = 0; + mWordCount = 0; + mIsEmptyUponStarting = true; + mIsEmptinessStateKnown = false; + mKeyCounter.reset(); + mBeforeDeleteKeyCounter.reset(); + mDuringRepeatedDeleteKeysCounter.reset(); + mAfterDeleteKeyCounter.reset(); + + mLastTapTime = 0; + mIsLastKeyDeleteKey = false; + } + + public void recordChar(int codePoint, long time) { + final long delta = time - mLastTapTime; + if (codePoint == Keyboard.CODE_DELETE) { + mDeleteKeyCount++; + if (delta < MIN_DELETION_INTERMISSION) { + if (mIsLastKeyDeleteKey) { + mDuringRepeatedDeleteKeysCounter.add(delta); + } else { + mBeforeDeleteKeyCounter.add(delta); + } + } + mIsLastKeyDeleteKey = true; + } else { + mCharCount++; + if (Character.isDigit(codePoint)) { + mNumberCount++; + } + if (Character.isLetter(codePoint)) { + mLetterCount++; + } + if (Character.isSpaceChar(codePoint)) { + mSpaceCount++; + } + if (mIsLastKeyDeleteKey && delta < MIN_DELETION_INTERMISSION) { + mAfterDeleteKeyCounter.add(delta); + } else if (!mIsLastKeyDeleteKey && delta < MIN_TYPING_INTERMISSION) { + mKeyCounter.add(delta); + } + mIsLastKeyDeleteKey = false; + } + mLastTapTime = time; + } + + public void recordWordEntered() { + mWordCount++; + } + + public void setIsEmptyUponStarting(final boolean isEmpty) { + mIsEmptyUponStarting = isEmpty; + mIsEmptinessStateKnown = true; + } +} diff --git a/java/src/com/android/inputmethod/research/UploaderService.java b/java/src/com/android/inputmethod/research/UploaderService.java new file mode 100644 index 000000000..7a5749096 --- /dev/null +++ b/java/src/com/android/inputmethod/research/UploaderService.java @@ -0,0 +1,191 @@ +/* + * 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.Manifest; +import android.app.AlarmManager; +import android.app.IntentService; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.os.BatteryManager; +import android.os.Bundle; +import android.util.Log; + +import com.android.inputmethod.latin.R; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileFilter; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; + +public final class UploaderService extends IntentService { + private static final String TAG = UploaderService.class.getSimpleName(); + public static final long RUN_INTERVAL = AlarmManager.INTERVAL_HOUR; + private 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; + + private boolean mCanUpload; + private File mFilesDir; + private URL mUrl; + + public UploaderService() { + super("Research Uploader Service"); + } + + @Override + public void onCreate() { + super.onCreate(); + + mCanUpload = false; + mFilesDir = null; + mUrl = null; + + final PackageManager packageManager = getPackageManager(); + final boolean hasPermission = packageManager.checkPermission(Manifest.permission.INTERNET, + getPackageName()) == PackageManager.PERMISSION_GRANTED; + if (!hasPermission) { + return; + } + + try { + final String urlString = getString(R.string.research_logger_upload_url); + if (urlString == null || urlString.equals("")) { + return; + } + mFilesDir = getFilesDir(); + mUrl = new URL(urlString); + mCanUpload = true; + } catch (MalformedURLException e) { + e.printStackTrace(); + } + } + + @Override + protected void onHandleIntent(Intent intent) { + if (!mCanUpload) { + return; + } + boolean isUploadingUnconditionally = false; + Bundle bundle = intent.getExtras(); + if (bundle != null && bundle.containsKey(EXTRA_UPLOAD_UNCONDITIONALLY)) { + isUploadingUnconditionally = bundle.getBoolean(EXTRA_UPLOAD_UNCONDITIONALLY); + } + doUpload(isUploadingUnconditionally); + } + + private boolean isExternallyPowered() { + final Intent intent = registerReceiver(null, new IntentFilter( + Intent.ACTION_BATTERY_CHANGED)); + final int pluggedState = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1); + return pluggedState == BatteryManager.BATTERY_PLUGGED_AC + || pluggedState == BatteryManager.BATTERY_PLUGGED_USB; + } + + private boolean hasWifiConnection() { + final ConnectivityManager manager = + (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); + final NetworkInfo wifiInfo = manager.getNetworkInfo(ConnectivityManager.TYPE_WIFI); + return wifiInfo.isConnected(); + } + + private void doUpload(final boolean isUploadingUnconditionally) { + if (!isUploadingUnconditionally && (!isExternallyPowered() || !hasWifiConnection())) { + return; + } + if (mFilesDir == null) { + return; + } + final File[] files = mFilesDir.listFiles(new FileFilter() { + @Override + public boolean accept(File pathname) { + return pathname.getName().startsWith(ResearchLogger.FILENAME_PREFIX) + && !pathname.canWrite(); + } + }); + boolean success = true; + if (files.length == 0) { + success = false; + } + for (final File file : files) { + if (!uploadFile(file)) { + success = false; + } + } + } + + private boolean uploadFile(File file) { + Log.d(TAG, "attempting upload of " + file.getAbsolutePath()); + boolean success = false; + final int contentLength = (int) file.length(); + HttpURLConnection connection = null; + InputStream fileInputStream = null; + try { + fileInputStream = new FileInputStream(file); + connection = (HttpURLConnection) mUrl.openConnection(); + connection.setRequestMethod("PUT"); + connection.setDoOutput(true); + connection.setFixedLengthStreamingMode(contentLength); + final OutputStream os = connection.getOutputStream(); + final byte[] buf = new byte[BUF_SIZE]; + int numBytesRead; + while ((numBytesRead = fileInputStream.read(buf)) != -1) { + os.write(buf, 0, numBytesRead); + } + if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) { + Log.d(TAG, "upload failed: " + connection.getResponseCode()); + InputStream netInputStream = connection.getInputStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(netInputStream)); + String line; + while ((line = reader.readLine()) != null) { + Log.d(TAG, "| " + reader.readLine()); + } + reader.close(); + return success; + } + file.delete(); + success = true; + Log.d(TAG, "upload successful"); + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (fileInputStream != null) { + try { + fileInputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + if (connection != null) { + connection.disconnect(); + } + } + return success; + } +} |