diff options
author | 2012-09-18 12:07:33 +0900 | |
---|---|---|
committer | 2012-09-18 12:07:33 +0900 | |
commit | f18fc03621b70f5a51cf54c4bf40eb213de40652 (patch) | |
tree | e6897145da33c21866b57c95444a530488ca2af1 /java/src | |
parent | 9761fa578609b4f3788344b5b3c886b1e883e97e (diff) | |
parent | 764dd712032d7b8012797b1116b523bef7b907f3 (diff) | |
download | latinime-f18fc03621b70f5a51cf54c4bf40eb213de40652.tar.gz latinime-f18fc03621b70f5a51cf54c4bf40eb213de40652.tar.xz latinime-f18fc03621b70f5a51cf54c4bf40eb213de40652.zip |
Merge remote-tracking branch 'goog/jb-mr1-dev' into mergescriptpackage
Diffstat (limited to 'java/src')
72 files changed, 5254 insertions, 3193 deletions
diff --git a/java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java b/java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java index 039c77b9c..5af5d044f 100644 --- a/java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java +++ b/java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java @@ -196,8 +196,7 @@ public class AccessibilityEntityProvider extends AccessibilityNodeProviderCompat info.setSource(mKeyboardView, virtualViewId); info.setBoundsInScreen(boundsInScreen); info.setEnabled(true); - info.setClickable(true); - info.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK); + info.setVisibleToUser(true); if (mAccessibilityFocusedView == virtualViewId) { info.addAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS); @@ -226,6 +225,9 @@ public class AccessibilityEntityProvider extends AccessibilityNodeProviderCompat mKeyboardView.onTouchEvent(downEvent); mKeyboardView.onTouchEvent(upEvent); + + downEvent.recycle(); + upEvent.recycle(); } @Override @@ -252,9 +254,6 @@ public class AccessibilityEntityProvider extends AccessibilityNodeProviderCompat final int virtualViewId = generateVirtualViewIdForKey(key); switch (action) { - case AccessibilityNodeInfoCompat.ACTION_CLICK: - simulateKeyPress(key); - return true; case AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS: if (mAccessibilityFocusedView == virtualViewId) { return false; 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 e42de0b4c..01220a58a 100644 --- a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java +++ b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java @@ -105,8 +105,21 @@ public class AccessibleKeyboardViewProxy extends AccessibilityDelegateCompat { } /** - * Receives hover events when accessibility is turned on in SDK versions ICS - * and higher. + * Intercepts touch events before dispatch when touch exploration is turned + * on in ICS and higher. + * + * @param event The motion event being dispatched. + * @return {@code true} if the event is handled + */ + public boolean dispatchTouchEvent(MotionEvent event) { + // To avoid accidental key presses during touch exploration, always drop + // touch events generated by the user. + return false; + } + + /** + * Receives hover events when touch exploration is turned on in SDK versions + * ICS and higher. * * @param event The hover event. * @return {@code true} if the event is handled 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 6ba309fcb..159f43650 100644 --- a/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java +++ b/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java @@ -33,7 +33,7 @@ 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 = 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..cb120a33e 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; @@ -47,14 +52,13 @@ import java.util.Locale; /** * Class for describing the position and characteristics of a single key in the keyboard. */ -public class Key { +public class Key implements Comparable<Key> { private static final String TAG = Key.class.getSimpleName(); /** * 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 equalsInternal(final Key o) { if (this == o) return true; return o.mX == mX && o.mY == mY @@ -397,19 +422,26 @@ 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; } @Override + public int compareTo(Key o) { + if (equalsInternal(o)) return 0; + if (mHashCode > o.mHashCode) return 1; + return -1; + } + + @Override public int hashCode() { return mHashCode; } @Override - public boolean equals(Object o) { - return o instanceof Key && equals((Key)o); + public boolean equals(final Object o) { + return o instanceof Key && equalsInternal((Key)o); } @Override @@ -425,7 +457,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 +468,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 +488,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 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 boolean isAlignLeft() { + 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 +658,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 +694,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 +708,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 +720,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 +786,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 +802,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 868c8cab5..f5686dcda 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyDetector.java +++ b/java/src/com/android/inputmethod/keyboard/KeyDetector.java @@ -83,11 +83,17 @@ public class KeyDetector { int minDistance = Integer.MAX_VALUE; Key primaryKey = null; for (final Key key: mKeyboard.getNearestKeys(touchX, touchY)) { - final boolean isOnKey = key.isOnKey(touchX, touchY); + // An edge key always has its enlarged hitbox to respond to an event that occurred in + // the empty area around the key. (@see Key#markAsLeftEdge(KeyboardParams)} etc.) + if (!key.isOnKey(touchX, touchY)) { + continue; + } final int distance = key.squaredDistanceToEdge(touchX, touchY); + if (distance > minDistance) { + continue; + } // To take care of hitbox overlaps, we compare mCode here too. - if (primaryKey == null || distance < minDistance - || (distance == minDistance && isOnKey && key.mCode > primaryKey.mCode)) { + if (primaryKey == null || distance < minDistance || key.mCode > primaryKey.mCode) { minDistance = distance; primaryKey = key; } diff --git a/java/src/com/android/inputmethod/keyboard/Keyboard.java b/java/src/com/android/inputmethod/keyboard/Keyboard.java index e37868b3f..b7c7f415d 100644 --- a/java/src/com/android/inputmethod/keyboard/Keyboard.java +++ b/java/src/com/android/inputmethod/keyboard/Keyboard.java @@ -16,39 +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.keyboard.internal.KeyboardParams; import com.android.inputmethod.latin.CollectionUtils; -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 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 @@ -81,6 +57,8 @@ public class Keyboard { public static final int CODE_DASH = '-'; public static final int CODE_SINGLE_QUOTE = '\''; public static final int CODE_DOUBLE_QUOTE = '"'; + public static final int CODE_QUESTION_MARK = '?'; + public static final int CODE_EXCLAMATION_MARK = '!'; // TODO: Check how this should work for right-to-left languages. It seems to stand // that for rtl languages, a closing parenthesis is a left parenthesis. Is this // managed by the font? Or is it a different char? @@ -120,6 +98,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; @@ -140,7 +121,7 @@ public class Keyboard { 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; @@ -149,7 +130,7 @@ public class Keyboard { mMostCommonKeyWidth = params.mMostCommonKeyWidth; mMoreKeysTemplate = params.mMoreKeysTemplate; mMaxMoreKeysKeyboardColumn = params.mMaxMoreKeysKeyboardColumn; - + mKeyVisualAttributes = params.mKeyVisualAttributes; mTopPadding = params.mTopPadding; mVerticalGap = params.mVerticalGap; @@ -165,7 +146,7 @@ public class Keyboard { mProximityCharsCorrectionEnabled = params.mProximityCharsCorrectionEnabled; } - public boolean hasProximityCharsCorrection(int code) { + public boolean hasProximityCharsCorrection(final int code) { if (!mProximityCharsCorrectionEnabled) { return false; } @@ -181,7 +162,7 @@ public class Keyboard { return mProximityInfo; } - public Key getKey(int code) { + public Key getKey(final int code) { if (code == CODE_UNSPECIFIED) { return null; } @@ -202,7 +183,7 @@ public class Keyboard { } } - public boolean hasKey(Key aKey) { + public boolean hasKey(final Key aKey) { if (mKeyCache.indexOfValue(aKey) >= 0) { return true; } @@ -216,7 +197,7 @@ public class Keyboard { return false; } - public static boolean isLetterCode(int code) { + public static boolean isLetterCode(final int code) { return code >= CODE_SPACE; } @@ -225,171 +206,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 = 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 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 @@ -397,14 +213,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"; @@ -426,944 +242,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/KeyboardLayoutSet.java b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java index 76ac3de22..aaccf63ba 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java +++ b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java @@ -35,7 +35,9 @@ 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; @@ -78,31 +80,19 @@ public class KeyboardLayoutSet { 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 = CollectionUtils.newHashMap(); - - 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; @@ -118,11 +108,7 @@ public class KeyboardLayoutSet { // Sparse array of KeyboardLayoutSet element parameters indexed by element's id. final SparseArray<ElementParams> mKeyboardLayoutSetElementIdToParamsMap = CollectionUtils.newSparseArray(); - - static class ElementParams { - int mKeyboardXmlId; - boolean mProximityCharsCorrectionEnabled; - } + public Params() {} } public static void clearKeyboardCache() { @@ -130,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: @@ -170,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); } @@ -202,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); @@ -225,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(); @@ -238,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; @@ -246,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( @@ -263,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); @@ -277,7 +264,7 @@ public class KeyboardLayoutSet { return this; } - public void setTouchPositionCorrectionEnabled(boolean enabled) { + public void setTouchPositionCorrectionEnabled(final boolean enabled) { mParams.mTouchPositionCorrectionEnabled = enabled; } @@ -298,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 { @@ -318,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) { @@ -340,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); @@ -367,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/KeyboardView.java b/java/src/com/android/inputmethod/keyboard/KeyboardView.java index 127ac7160..cf89567f8 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyboardView.java +++ b/java/src/com/android/inputmethod/keyboard/KeyboardView.java @@ -38,6 +38,9 @@ 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; @@ -53,43 +56,64 @@ 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_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_gestureFloatingPreviewColor + * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewHorizontalPadding + * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewVerticalPadding + * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewRoundRadius + * @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 }; - private static final float UNDEFINED_RATIO = -1.0f; - private static final int UNDEFINED_DIMENSION = -1; - // 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. @@ -105,15 +129,44 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { // Main keyboard private Keyboard mKeyboard; - protected final KeyDrawParams mKeyDrawParams; + protected final KeyDrawParams mKeyDrawParams = new KeyDrawParams(); + + // Preview placer view + private final PreviewPlacerView mPreviewPlacerView; + private final int[] mCoordinates = new int[2]; // Key preview + private static final int PREVIEW_ALPHA = 240; private final int mKeyPreviewLayoutId; + private final int mPreviewOffset; + private final int mPreviewHeight; + private final int mPreviewLingerTimeout; private final SparseArray<TextView> mKeyPreviewTexts = CollectionUtils.newSparseArray(); - protected final KeyPreviewDrawParams mKeyPreviewDrawParams; + protected final KeyPreviewDrawParams mKeyPreviewDrawParams = new KeyPreviewDrawParams(); private boolean mShowKeyPreviewPopup = true; private int mDelayAfterPreview; - private final PreviewPlacerView mPreviewPlacerView; + // Background state set + private static final int[][][] KEY_PREVIEW_BACKGROUND_STATE_TABLE = { + { // STATE_MIDDLE + EMPTY_STATE_SET, + { R.attr.state_has_morekeys } + }, + { // STATE_LEFT + { R.attr.state_left_edge }, + { R.attr.state_left_edge, R.attr.state_has_morekeys } + }, + { // STATE_RIGHT + { R.attr.state_right_edge }, + { R.attr.state_right_edge, R.attr.state_has_morekeys } + } + }; + private static final int STATE_MIDDLE = 0; + private static final int STATE_LEFT = 1; + private static final int STATE_RIGHT = 2; + private static final int STATE_NORMAL = 0; + private static final int STATE_HAS_MOREKEYS = 1; + private static final int[] KEY_PREVIEW_BACKGROUND_DEFAULT_STATE = + KEY_PREVIEW_BACKGROUND_STATE_TABLE[STATE_MIDDLE][STATE_NORMAL]; // Drawing /** True if the entire keyboard needs to be dimmed. */ @@ -129,7 +182,7 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { private final Region mClipRegion = new Region(); private Bitmap mOffscreenBuffer; /** The canvas for the above mutable keyboard bitmap */ - private Canvas mOffscreenCanvas; + private final Canvas mOffscreenCanvas = new Canvas(); 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. @@ -144,12 +197,12 @@ 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; @@ -163,11 +216,11 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { } } - 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); } @@ -180,228 +233,60 @@ 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; - - 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(final TypedArray a) { - mKeyBackground = a.getDrawable(R.styleable.KeyboardView_keyBackground); - if (!isValidFraction(mKeyLetterRatio = getFraction(a, - R.styleable.KeyboardView_keyLetterSize))) { - mKeyLetterSize = getDimensionPixelSize(a, R.styleable.KeyboardView_keyLetterSize); - } - if (!isValidFraction(mKeyLabelRatio = getFraction(a, - R.styleable.KeyboardView_keyLabelSize))) { - mKeyLabelSize = getDimensionPixelSize(a, R.styleable.KeyboardView_keyLabelSize); - } - mKeyLargeLabelRatio = getFraction(a, R.styleable.KeyboardView_keyLargeLabelRatio); - mKeyLargeLetterRatio = getFraction(a, R.styleable.KeyboardView_keyLargeLetterRatio); - mKeyHintLetterRatio = getFraction(a, R.styleable.KeyboardView_keyHintLetterRatio); - mKeyShiftedLetterHintRatio = getFraction(a, - R.styleable.KeyboardView_keyShiftedLetterHintRatio); - mKeyHintLabelRatio = getFraction(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 (isValidFraction(mKeyLetterRatio)) { - mKeyLetterSize = (int)(keyHeight * mKeyLetterRatio); - } - if (isValidFraction(mKeyLabelRatio)) { - 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 = getFraction(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); - mDelayAfterPreview = mKeyPreviewDrawParams.mLingerTimeout; - 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); + 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); - a.recycle(); + 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); } - static boolean isValidFraction(final float fraction) { - return fraction >= 0.0f; - } - - static float getFraction(final TypedArray a, final int index) { - final TypedValue value = a.peekValue(index); - if (value == null || value.type != TypedValue.TYPE_FRACTION) { - return UNDEFINED_RATIO; - } - return a.getFraction(index, 1, 1, UNDEFINED_RATIO); - } - - public static int getDimensionPixelSize(final TypedArray a, final int index) { - final TypedValue value = a.peekValue(index); - if (value == null || value.type != TypedValue.TYPE_DIMENSION) { - return UNDEFINED_DIMENSION; - } - return a.getDimensionPixelSize(index, UNDEFINED_DIMENSION); + 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)); } /** @@ -411,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); } /** @@ -437,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; } @@ -451,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(); @@ -469,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); @@ -480,7 +365,8 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { if (bufferNeedsUpdates || mOffscreenBuffer == null) { if (maybeAllocateOffscreenBuffer()) { mInvalidateAllKeys = true; - maybeCreateOffscreenCanvas(); + // TODO: Stop using the offscreen canvas even when in software rendering + mOffscreenCanvas.setBitmap(mOffscreenBuffer); } onDrawKeyboard(mOffscreenCanvas); } @@ -509,22 +395,12 @@ 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(); @@ -557,13 +433,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); } } } @@ -587,7 +463,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) { @@ -595,14 +471,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); @@ -610,14 +490,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(final Key key, final 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) { @@ -632,8 +512,9 @@ 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; + protected void onDrawKeyTopVisuals(final Key key, final Canvas canvas, final Paint paint, + final KeyDrawParams params) { + final int keyWidth = key.getDrawWidth(); final int keyHeight = key.mHeight; final float centerX = keyWidth * 0.5f; final float centerY = keyHeight * 0.5f; @@ -647,12 +528,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); @@ -662,10 +539,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? @@ -690,16 +567,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); @@ -727,25 +603,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 @@ -756,19 +617,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(); @@ -779,15 +640,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; @@ -808,17 +669,18 @@ 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; + protected void drawKeyPopupHint(final Key key, final Canvas canvas, final Paint paint, + final KeyDrawParams params) { + 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) { @@ -828,7 +690,7 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { } } - private static int getCharGeometryCacheKey(char referenceChar, Paint paint) { + private static int getCharGeometryCacheKey(final char referenceChar, final Paint paint) { final int labelSize = (int)paint.getTextSize(); final Typeface face = paint.getTypeface(); final int codePointOffset = referenceChar << 15; @@ -846,7 +708,7 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { // Working variable for the following methods. private final Rect mTextBounds = new Rect(); - private float getCharHeight(char[] referenceChar, Paint paint) { + private float getCharHeight(final char[] referenceChar, final Paint paint) { final int key = getCharGeometryCacheKey(referenceChar[0], paint); final Float cachedValue = sTextHeightCache.get(key); if (cachedValue != null) @@ -858,7 +720,7 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { return height; } - private float getCharWidth(char[] referenceChar, Paint paint) { + private float getCharWidth(final char[] referenceChar, final Paint paint) { final int key = getCharGeometryCacheKey(referenceChar[0], paint); final Float cachedValue = sTextWidthCache.get(key); if (cachedValue != null) @@ -870,36 +732,37 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { return width; } - public float getLabelWidth(String label, Paint paint) { - paint.getTextBounds(label.toString(), 0, label.length(), mTextBounds); + public float getLabelWidth(final String label, final Paint paint) { + paint.getTextBounds(label, 0, label.length(), mTextBounds); return mTextBounds.width(); } - protected static void drawIcon(Canvas canvas, Drawable icon, int x, int y, int width, - int height) { + protected static void drawIcon(final Canvas canvas, final Drawable icon, final int x, + final int y, final int width, final int height) { canvas.translate(x, y); icon.setBounds(0, 0, width, height); icon.draw(canvas); canvas.translate(-x, -y); } - private static void drawHorizontalLine(Canvas canvas, float y, float w, int color, - Paint paint) { + private static void drawHorizontalLine(final Canvas canvas, final float y, final float w, + final int color, final Paint paint) { paint.setStyle(Paint.Style.STROKE); paint.setStrokeWidth(1.0f); paint.setColor(color); canvas.drawLine(0, y, w, y, paint); } - private static void drawVerticalLine(Canvas canvas, float x, float h, int color, Paint paint) { + private static void drawVerticalLine(final Canvas canvas, final float x, final float h, + final int color, final Paint paint) { paint.setStyle(Paint.Style.STROKE); paint.setStrokeWidth(1.0f); paint.setColor(color); canvas.drawLine(x, 0, x, h, paint); } - private static void drawRectangle(Canvas canvas, float x, float y, float w, float h, int color, - Paint paint) { + private static void drawRectangle(final Canvas canvas, final float x, final float y, + final float w, final float h, final int color, final Paint paint) { paint.setStyle(Paint.Style.STROKE); paint.setStrokeWidth(1.0f); paint.setColor(color); @@ -911,8 +774,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; } @@ -950,11 +813,11 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { } @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)); @@ -981,7 +844,7 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { } } - public void showGestureFloatingPreviewText(String gestureFloatingPreviewText) { + public void showGestureFloatingPreviewText(final String gestureFloatingPreviewText) { locatePreviewPlacerView(); mPreviewPlacerView.setGestureFloatingPreviewText(gestureFloatingPreviewText); } @@ -992,15 +855,19 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { } @Override - public void showGesturePreviewTrail(PointerTracker tracker) { + public void showGesturePreviewTrail(final PointerTracker tracker, + final boolean isOldestTracker) { locatePreviewPlacerView(); - mPreviewPlacerView.invalidatePointer(tracker); + mPreviewPlacerView.invalidatePointer(tracker, isOldestTracker); } - @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 = getKeyPreviewText(tracker.mPointerId); // If the key preview has no parent view yet, add it to the ViewGroup which can place @@ -1014,21 +881,28 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { // If key is invalid or IME is already closed, we must not show key preview. // Trying to show key preview while root window is closed causes // WindowManager.BadTokenException. - if (key == null) + if (key == null) { return; + } - final KeyPreviewDrawParams params = mKeyPreviewDrawParams; + final KeyDrawParams drawParams = mKeyDrawParams; + previewText.setTextColor(drawParams.mPreviewTextColor); + final Drawable background = previewText.getBackground(); + if (background != null) { + background.setState(KEY_PREVIEW_BACKGROUND_DEFAULT_STATE); + background.setAlpha(PREVIEW_ALPHA); + } 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 { @@ -1036,48 +910,44 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { key.getPreviewIcon(mKeyboard.mIconsSet)); previewText.setText(null); } - previewText.setBackgroundDrawable(params.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(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]; + final int statePosition; + int previewX = key.getDrawX() - (previewWidth - keyDrawWidth) / 2 + mCoordinates[0]; if (previewX < 0) { previewX = 0; - if (params.mPreviewLeftBackground != null) { - previewText.setBackgroundDrawable(params.mPreviewLeftBackground); - } + statePosition = STATE_LEFT; } else if (previewX > getWidth() - previewWidth) { previewX = getWidth() - previewWidth; - if (params.mPreviewRightBackground != null) { - previewText.setBackgroundDrawable(params.mPreviewRightBackground); - } + statePosition = STATE_RIGHT; + } else { + statePosition = STATE_MIDDLE; } // 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 + mCoordinates[1]; - // Set the preview background state - previewText.getBackground().setState( - key.mMoreKeys != null ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET); - previewText.setTextColor(params.mPreviewTextColor); + if (background != null) { + final int hasMoreKeys = (key.mMoreKeys != null) ? STATE_HAS_MOREKEYS : STATE_NORMAL; + background.setState(KEY_PREVIEW_BACKGROUND_STATE_TABLE[statePosition][hasMoreKeys]); + } ViewLayoutUtils.placeViewAt( previewText, previewX, previewY, previewWidth, previewHeight); previewText.setVisibility(VISIBLE); @@ -1103,7 +973,7 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { * @see #invalidateAllKeys */ @Override - public void invalidateKey(Key key) { + public void invalidateKey(final Key key) { if (mInvalidateAllKeys) return; if (key == null) return; mInvalidatedKeys.add(key); diff --git a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java index df84271e8..4ed0f58e1 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 { @@ -149,7 +166,7 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key } @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) { @@ -173,14 +190,14 @@ 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) { + public void startKeyRepeatTimer(final PointerTracker tracker) { startKeyRepeatTimer(tracker, mKeyRepeatStartTimeout); } @@ -194,7 +211,7 @@ 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) { @@ -211,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; @@ -265,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; } @@ -321,11 +338,11 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key } } - 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); @@ -334,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); @@ -376,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); @@ -391,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); } @@ -400,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); } @@ -442,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); @@ -467,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); } @@ -483,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; } @@ -514,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; @@ -529,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; @@ -555,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(); } @@ -579,20 +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) { + 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); @@ -618,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()); @@ -643,7 +661,15 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key } @Override - public boolean onTouchEvent(MotionEvent me) { + public boolean dispatchTouchEvent(MotionEvent event) { + if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) { + return AccessibleKeyboardViewProxy.getInstance().dispatchTouchEvent(event); + } + return super.dispatchTouchEvent(event); + } + + @Override + public boolean onTouchEvent(final MotionEvent me) { if (getKeyboard() == null) { return false; } @@ -651,7 +677,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(); @@ -818,7 +844,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); @@ -828,7 +854,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); @@ -845,8 +871,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; @@ -868,14 +894,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; } @@ -893,7 +920,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; @@ -906,7 +933,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()); @@ -927,7 +954,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; @@ -982,7 +1009,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); } @@ -991,7 +1018,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 ""; } @@ -1000,7 +1027,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/PointerTracker.java b/java/src/com/android/inputmethod/keyboard/PointerTracker.java index be101cfb0..a6439c46a 100644 --- a/java/src/com/android/inputmethod/keyboard/PointerTracker.java +++ b/java/src/com/android/inputmethod/keyboard/PointerTracker.java @@ -80,7 +80,7 @@ public class PointerTracker implements PointerTrackerQueue.Element { public void invalidateKey(Key key); public void showKeyPreview(PointerTracker tracker); public void dismissKeyPreview(PointerTracker tracker); - public void showGesturePreviewTrail(PointerTracker tracker); + public void showGesturePreviewTrail(PointerTracker tracker, boolean isOldestTracker); } public interface TimerProxy { @@ -330,10 +330,10 @@ public class PointerTracker implements PointerTrackerQueue.Element { 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()); } @@ -347,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); } @@ -440,13 +440,13 @@ 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); } } @@ -479,13 +479,13 @@ 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); } } @@ -545,12 +545,16 @@ public class PointerTracker implements PointerTrackerQueue.Element { } private void startBatchInput() { + if (sInGesture || !mGestureStrokeWithPreviewTrail.isStartOfAGesture()) { + return; + } if (DEBUG_LISTENER) { Log.d(TAG, "onStartBatchInput"); } sInGesture = true; mListener.onStartBatchInput(); - mDrawingProxy.showGesturePreviewTrail(this); + final boolean isOldestTracker = sPointerTrackerQueue.getOldestElement() == this; + mDrawingProxy.showGesturePreviewTrail(this, isOldestTracker); } private void updateBatchInput(final long eventTime) { @@ -567,12 +571,14 @@ public class PointerTracker implements PointerTrackerQueue.Element { mListener.onUpdateBatchInput(sAggregratedPointers); } } - mDrawingProxy.showGesturePreviewTrail(this); + final boolean isOldestTracker = sPointerTrackerQueue.getOldestElement() == this; + mDrawingProxy.showGesturePreviewTrail(this, isOldestTracker); } private void endBatchInput() { synchronized (sAggregratedPointers) { mGestureStrokeWithPreviewTrail.appendAllBatchPoints(sAggregratedPointers); + mGestureStrokeWithPreviewTrail.reset(); if (getActivePointerTrackerCount() == 1) { if (DEBUG_LISTENER) { Log.d(TAG, "onEndBatchInput: batchPoints=" @@ -583,7 +589,8 @@ public class PointerTracker implements PointerTrackerQueue.Element { clearBatchInputPointsOfAllPointerTrackers(); } } - mDrawingProxy.showGesturePreviewTrail(this); + final boolean isOldestTracker = sPointerTrackerQueue.getOldestElement() == this; + mDrawingProxy.showGesturePreviewTrail(this, isOldestTracker); } private static void abortBatchInput() { @@ -719,12 +726,8 @@ public class PointerTracker implements PointerTrackerQueue.Element { 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 (!sInGesture && stroke.isStartOfAGesture()) { - startBatchInput(); - } - + mGestureStrokeWithPreviewTrail.addPoint(x, y, gestureTime, isHistorical); + startBatchInput(); if (sInGesture && key != null) { updateBatchInput(eventTime); } diff --git a/java/src/com/android/inputmethod/keyboard/ProximityInfo.java b/java/src/com/android/inputmethod/keyboard/ProximityInfo.java index 71bf31faa..e1b082c16 100644 --- a/java/src/com/android/inputmethod/keyboard/ProximityInfo.java +++ b/java/src/com/android/inputmethod/keyboard/ProximityInfo.java @@ -19,7 +19,7 @@ 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; @@ -48,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 { @@ -81,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("", 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 index e814d8009..4311fa775 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/GesturePreviewTrail.java +++ b/java/src/com/android/inputmethod/keyboard/internal/GesturePreviewTrail.java @@ -17,44 +17,53 @@ package com.android.inputmethod.keyboard.internal; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Paint; +import android.graphics.Rect; import android.os.SystemClock; import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.ResizableIntArray; -class GesturePreviewTrail { +final 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; + // The wall time of the zero value in {@link #mEventTimes} + private long mCurrentTimeBase; 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 { + static final class Params { + public final int mTrailColor; + public final float mTrailStartWidth; + public final float mTrailEndWidth; public final int mFadeoutStartDelay; public final int mFadeoutDuration; public final int mUpdateInterval; - public GesturePreviewTrailParams(final TypedArray keyboardViewAttr) { + public final int mTrailLingerDuration; + + public Params(final TypedArray keyboardViewAttr) { + mTrailColor = keyboardViewAttr.getColor( + R.styleable.KeyboardView_gesturePreviewTrailColor, 0); + mTrailStartWidth = keyboardViewAttr.getDimension( + R.styleable.KeyboardView_gesturePreviewTrailStartWidth, 0.0f); + mTrailEndWidth = keyboardViewAttr.getDimension( + R.styleable.KeyboardView_gesturePreviewTrailEndWidth, 0.0f); mFadeoutStartDelay = keyboardViewAttr.getInt( R.styleable.KeyboardView_gesturePreviewTrailFadeoutStartDelay, 0); mFadeoutDuration = keyboardViewAttr.getInt( R.styleable.KeyboardView_gesturePreviewTrailFadeoutDuration, 0); + mTrailLingerDuration = mFadeoutStartDelay + mFadeoutDuration; mUpdateInterval = keyboardViewAttr.getInt( R.styleable.KeyboardView_gesturePreviewTrailUpdateInterval, 0); } } - public GesturePreviewTrail(final GesturePreviewTrailParams params) { - mPreviewParams = params; - } + // Use this value as imaginary zero because x-coordinates may be zero. + private static final int DOWN_EVENT_MARKER = -128; private static int markAsDownEvent(final int xCoord) { return DOWN_EVENT_MARKER - xCoord; @@ -70,47 +79,53 @@ class GesturePreviewTrail { } 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) { + if (mEventTimes.getLength() == trailSize) { return; } - if (isNewStroke) { - final int elapsedTime = (int)(downTime - mCurrentDownTime); - final int[] eventTimes = mEventTimes.getPrimitiveArray(); + final int[] eventTimes = mEventTimes.getPrimitiveArray(); + final int strokeId = stroke.getGestureStrokeId(); + if (strokeId != mCurrentStrokeId) { + final int elapsedTime = (int)(downTime - mCurrentTimeBase); for (int i = mTrailStartIndex; i < trailSize; i++) { + // Decay the previous strokes' event times. eventTimes[i] -= elapsedTime; } - - if (newTrailSize > trailSize) { - final int[] xCoords = mXCoordinates.getPrimitiveArray(); - xCoords[trailSize] = markAsDownEvent(xCoords[trailSize]); - } - mCurrentDownTime = downTime; + final int[] xCoords = mXCoordinates.getPrimitiveArray(); + final int downIndex = trailSize; + xCoords[downIndex] = markAsDownEvent(xCoords[downIndex]); + mCurrentTimeBase = downTime - eventTimes[downIndex]; mCurrentStrokeId = strokeId; } } - private int getAlpha(final int elapsedTime) { - if (elapsedTime < mPreviewParams.mFadeoutStartDelay) { + private static int getAlpha(final int elapsedTime, final Params params) { + if (elapsedTime < params.mFadeoutStartDelay) { return Constants.Color.ALPHA_OPAQUE; } final int decreasingAlpha = Constants.Color.ALPHA_OPAQUE - * (elapsedTime - mPreviewParams.mFadeoutStartDelay) - / mPreviewParams.mFadeoutDuration; + * (elapsedTime - params.mFadeoutStartDelay) + / params.mFadeoutDuration; return Constants.Color.ALPHA_OPAQUE - decreasingAlpha; } + private static float getWidth(final int elapsedTime, final Params params) { + return Math.max((params.mTrailLingerDuration - elapsedTime) + * (params.mTrailStartWidth - params.mTrailEndWidth) + / params.mTrailLingerDuration, 0.0f); + } + /** * 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 + * @param outBoundsRect the bounding box of this gesture trail drawing + * @param params The drawing parameters of gesture preview trail * @return true if some gesture preview trails remain to be drawn */ - public boolean drawGestureTrail(final Canvas canvas, final Paint paint) { + public boolean drawGestureTrail(final Canvas canvas, final Paint paint, + final Rect outBoundsRect, final Params params) { final int trailSize = mEventTimes.getLength(); if (trailSize == 0) { return false; @@ -119,34 +134,47 @@ class GesturePreviewTrail { 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; + final int sinceDown = (int)(SystemClock.uptimeMillis() - mCurrentTimeBase); int startIndex; for (startIndex = mTrailStartIndex; startIndex < trailSize; startIndex++) { final int elapsedTime = sinceDown - eventTimes[startIndex]; // Skip too old trail points. - if (elapsedTime < lingeringDuration) { + if (elapsedTime < params.mTrailLingerDuration) { break; } } mTrailStartIndex = startIndex; if (startIndex < trailSize) { + paint.setColor(params.mTrailColor); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeCap(Paint.Cap.ROUND); int lastX = getXCoordValue(xCoords[startIndex]); int lastY = yCoords[startIndex]; + float maxWidth = getWidth(sinceDown - eventTimes[startIndex], params); + // Initialize bounds rectangle. + outBoundsRect.set(lastX, lastY, lastX, lastY); 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)); + final int alpha = getAlpha(elapsedTime, params); + paint.setAlpha(alpha); + final float width = getWidth(elapsedTime, params); + paint.setStrokeWidth(width); canvas.drawLine(lastX, lastY, x, y, paint); + // Take union for the bounds. + outBoundsRect.union(x, y); + maxWidth = Math.max(maxWidth, width); } lastX = getXCoordValue(x); lastY = y; } + // Take care of trail line width. + final int inset = -((int)maxWidth + 1); + outBoundsRect.inset(inset, inset); } final int newSize = trailSize - startIndex; 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..5dcd842f7 --- /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 final 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..609d1a57f --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewDrawParams.java @@ -0,0 +1,44 @@ +/* + * 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 final 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; +} diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java b/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java index 13214bb9f..2a57caa5f 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java @@ -56,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)) { @@ -117,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; } @@ -136,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) { @@ -157,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; } @@ -170,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; @@ -181,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; } @@ -205,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) { @@ -230,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())); @@ -241,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()) @@ -251,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(); } @@ -268,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; } @@ -289,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; @@ -357,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; @@ -408,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); @@ -421,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) { @@ -460,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; } @@ -486,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; } @@ -502,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( @@ -512,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 e40cf45cc..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,6 @@ 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; @@ -30,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 = CollectionUtils.newHashMap(); + 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 = 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); } @@ -111,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); } @@ -124,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); } @@ -137,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) { @@ -147,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); @@ -165,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()); @@ -211,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/KeyboardParams.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardParams.java new file mode 100644 index 000000000..e6fe50e02 --- /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.TreeSet; + +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 TreeSet<Key> mKeys = CollectionUtils.newTreeSet(); // ordered set + 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 a608cdef0..3b7c6ad7a 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java @@ -210,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 = ""; @@ -348,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 */ @@ -857,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 @@ -2695,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..550391b49 --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java @@ -0,0 +1,86 @@ +/* + * 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.text.TextUtils; + +import com.android.inputmethod.keyboard.Keyboard; +import com.android.inputmethod.latin.StringUtils; + +import java.util.Locale; + +public final 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 int hashCode() { + int hashCode = 1; + hashCode = 31 + mCode; + hashCode = hashCode * 31 + mIconId; + hashCode = hashCode * 31 + (mLabel == null ? 0 : mLabel.hashCode()); + hashCode = hashCode * 31 + (mOutputText == null ? 0 : mOutputText.hashCode()); + return hashCode; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o instanceof MoreKeySpec) { + final MoreKeySpec other = (MoreKeySpec)o; + return mCode == other.mCode + && mIconId == other.mIconId + && TextUtils.equals(mLabel, other.mLabel) + && TextUtils.equals(mOutputText, other.mOutputText); + } + return false; + } + + @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 e0858c019..c1a5cbead 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java +++ b/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java @@ -73,6 +73,10 @@ public class PointerTrackerQueue { mArraySize = newSize; } + public synchronized Element getOldestElement() { + return (mArraySize == 0) ? null : mExpandableArrayOfActivePointers.get(0); + } + public synchronized void releaseAllPointersOlderThan(final Element pointer, final long eventTime) { if (DEBUG) { diff --git a/java/src/com/android/inputmethod/keyboard/internal/PreviewPlacerView.java b/java/src/com/android/inputmethod/keyboard/internal/PreviewPlacerView.java index 269b202b5..3a850096f 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/PreviewPlacerView.java +++ b/java/src/com/android/inputmethod/keyboard/internal/PreviewPlacerView.java @@ -18,9 +18,15 @@ package com.android.inputmethod.keyboard.internal; import android.content.Context; import android.content.res.TypedArray; +import android.graphics.Bitmap; import android.graphics.Canvas; +import android.graphics.Color; import android.graphics.Paint; import android.graphics.Paint.Align; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; +import android.graphics.RectF; import android.os.Message; import android.text.TextUtils; import android.util.AttributeSet; @@ -28,22 +34,18 @@ 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.keyboard.internal.GesturePreviewTrail.Params; import com.android.inputmethod.latin.CollectionUtils; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.StaticInnerHandlerWrapper; public class PreviewPlacerView extends RelativeLayout { - private final Paint mGesturePaint; - private final Paint mTextPaint; private final int mGestureFloatingPreviewTextColor; private final int mGestureFloatingPreviewTextOffset; - private final int mGestureFloatingPreviewTextShadowColor; - private final int mGestureFloatingPreviewTextShadowBorder; - private final int mGestureFloatingPreviewTextShadingColor; - private final int mGestureFloatingPreviewTextShadingBorder; - private final int mGestureFloatingPreviewTextConnectorColor; - private final int mGestureFloatingPreviewTextConnectorWidth; + private final int mGestureFloatingPreviewColor; + private final float mGestureFloatingPreviewHorizontalPadding; + private final float mGestureFloatingPreviewVerticalPadding; + private final float mGestureFloatingPreviewRoundRadius; /* package */ final int mGestureFloatingPreviewTextLingerTimeout; private int mXOrigin; @@ -51,13 +53,22 @@ public class PreviewPlacerView extends RelativeLayout { private final SparseArray<GesturePreviewTrail> mGesturePreviewTrails = CollectionUtils.newSparseArray(); - private final GesturePreviewTrailParams mGesturePreviewTrailParams; + private final Params mGesturePreviewTrailParams; + private final Paint mGesturePaint; + private boolean mDrawsGesturePreviewTrail; + private Bitmap mOffscreenBuffer; + private final Canvas mOffscreenCanvas = new Canvas(); + private final Rect mOffscreenDirtyRect = new Rect(); + private final Rect mGesturePreviewTrailBoundsRect = new Rect(); // per trail + private final Paint mTextPaint; private String mGestureFloatingPreviewText; + private final int mGestureFloatingPreviewTextHeight; + // {@link RectF} is needed for {@link Canvas#drawRoundRect(RectF, float, float, Paint)}. + private final RectF mGestureFloatingPreviewRectangle = new RectF(); private int mLastPointerX; private int mLastPointerY; - - private boolean mDrawsGesturePreviewTrail; + private static final char[] TEXT_HEIGHT_REFERENCE_CHAR = { 'M' }; private boolean mDrawsGestureFloatingPreviewText; private final DrawingHandler mDrawingHandler; @@ -66,10 +77,10 @@ public class PreviewPlacerView extends RelativeLayout { 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; + private final Params mGesturePreviewTrailParams; public DrawingHandler(final PreviewPlacerView outerInstance, - final GesturePreviewTrailParams gesturePreviewTrailParams) { + final Params gesturePreviewTrailParams) { super(outerInstance); mGesturePreviewTrailParams = gesturePreviewTrailParams; } @@ -132,41 +143,38 @@ public class PreviewPlacerView extends RelativeLayout { R.styleable.KeyboardView_gestureFloatingPreviewTextColor, 0); mGestureFloatingPreviewTextOffset = keyboardViewAttr.getDimensionPixelOffset( R.styleable.KeyboardView_gestureFloatingPreviewTextOffset, 0); - mGestureFloatingPreviewTextShadowColor = keyboardViewAttr.getColor( - R.styleable.KeyboardView_gestureFloatingPreviewTextShadowColor, 0); - mGestureFloatingPreviewTextShadowBorder = keyboardViewAttr.getDimensionPixelSize( - R.styleable.KeyboardView_gestureFloatingPreviewTextShadowBorder, 0); - mGestureFloatingPreviewTextShadingColor = keyboardViewAttr.getColor( - R.styleable.KeyboardView_gestureFloatingPreviewTextShadingColor, 0); - mGestureFloatingPreviewTextShadingBorder = keyboardViewAttr.getDimensionPixelSize( - R.styleable.KeyboardView_gestureFloatingPreviewTextShadingBorder, 0); - mGestureFloatingPreviewTextConnectorColor = keyboardViewAttr.getColor( - R.styleable.KeyboardView_gestureFloatingPreviewTextConnectorColor, 0); - mGestureFloatingPreviewTextConnectorWidth = keyboardViewAttr.getDimensionPixelSize( - R.styleable.KeyboardView_gestureFloatingPreviewTextConnectorWidth, 0); + mGestureFloatingPreviewColor = keyboardViewAttr.getColor( + R.styleable.KeyboardView_gestureFloatingPreviewColor, 0); + mGestureFloatingPreviewHorizontalPadding = keyboardViewAttr.getDimension( + R.styleable.KeyboardView_gestureFloatingPreviewHorizontalPadding, 0.0f); + mGestureFloatingPreviewVerticalPadding = keyboardViewAttr.getDimension( + R.styleable.KeyboardView_gestureFloatingPreviewVerticalPadding, 0.0f); + mGestureFloatingPreviewRoundRadius = keyboardViewAttr.getDimension( + R.styleable.KeyboardView_gestureFloatingPreviewRoundRadius, 0.0f); mGestureFloatingPreviewTextLingerTimeout = keyboardViewAttr.getInt( R.styleable.KeyboardView_gestureFloatingPreviewTextLingerTimeout, 0); - final int gesturePreviewTrailColor = keyboardViewAttr.getColor( - R.styleable.KeyboardView_gesturePreviewTrailColor, 0); - final int gesturePreviewTrailWidth = keyboardViewAttr.getDimensionPixelSize( - R.styleable.KeyboardView_gesturePreviewTrailWidth, 0); - mGesturePreviewTrailParams = new GesturePreviewTrailParams(keyboardViewAttr); + mGesturePreviewTrailParams = new Params(keyboardViewAttr); keyboardViewAttr.recycle(); mDrawingHandler = new DrawingHandler(this, mGesturePreviewTrailParams); - mGesturePaint = new Paint(); - mGesturePaint.setAntiAlias(true); - mGesturePaint.setStyle(Paint.Style.STROKE); - mGesturePaint.setStrokeJoin(Paint.Join.ROUND); - mGesturePaint.setColor(gesturePreviewTrailColor); - mGesturePaint.setStrokeWidth(gesturePreviewTrailWidth); - - mTextPaint = new Paint(); - mTextPaint.setAntiAlias(true); - mTextPaint.setStrokeJoin(Paint.Join.ROUND); - mTextPaint.setTextAlign(Align.CENTER); - mTextPaint.setTextSize(gestureFloatingPreviewTextSize); + final Paint gesturePaint = new Paint(); + gesturePaint.setAntiAlias(true); + gesturePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC)); + mGesturePaint = gesturePaint; + + final Paint textPaint = new Paint(); + textPaint.setAntiAlias(true); + textPaint.setTextAlign(Align.CENTER); + textPaint.setTextSize(gestureFloatingPreviewTextSize); + mTextPaint = textPaint; + final Rect textRect = new Rect(); + textPaint.getTextBounds(TEXT_HEIGHT_REFERENCE_CHAR, 0, 1, textRect); + mGestureFloatingPreviewTextHeight = textRect.height(); + + final Paint layerPaint = new Paint(); + layerPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER)); + setLayerType(LAYER_TYPE_HARDWARE, layerPaint); } public void setOrigin(final int x, final int y) { @@ -180,28 +188,50 @@ public class PreviewPlacerView extends RelativeLayout { mDrawsGestureFloatingPreviewText = drawsGestureFloatingPreviewText; } - public void invalidatePointer(final PointerTracker tracker) { + public void invalidatePointer(final PointerTracker tracker, final boolean isOldestTracker) { GesturePreviewTrail trail; synchronized (mGesturePreviewTrails) { trail = mGesturePreviewTrails.get(tracker.mPointerId); if (trail == null) { - trail = new GesturePreviewTrail(mGesturePreviewTrailParams); + trail = new GesturePreviewTrail(); mGesturePreviewTrails.put(tracker.mPointerId, trail); } } trail.addStroke(tracker.getGestureStrokeWithPreviewTrail(), tracker.getDownTime()); - mLastPointerX = tracker.getLastX(); - mLastPointerY = tracker.getLastY(); + if (isOldestTracker) { + mLastPointerX = tracker.getLastX(); + mLastPointerY = tracker.getLastY(); + } // TODO: Should narrow the invalidate region. invalidate(); } @Override + protected void onDetachedFromWindow() { + if (mOffscreenBuffer != null) { + mOffscreenBuffer.recycle(); + mOffscreenBuffer = null; + } + } + + @Override public void onDraw(final Canvas canvas) { super.onDraw(canvas); canvas.translate(mXOrigin, mYOrigin); if (mDrawsGesturePreviewTrail) { + if (mOffscreenBuffer == null) { + mOffscreenBuffer = Bitmap.createBitmap( + getWidth(), getHeight(), Bitmap.Config.ARGB_8888); + mOffscreenCanvas.setBitmap(mOffscreenBuffer); + } + if (!mOffscreenDirtyRect.isEmpty()) { + // Clear previous dirty rectangle. + mGesturePaint.setColor(Color.TRANSPARENT); + mGesturePaint.setStyle(Paint.Style.FILL); + mOffscreenCanvas.drawRect(mOffscreenDirtyRect, mGesturePaint); + mOffscreenDirtyRect.setEmpty(); + } boolean needsUpdatingGesturePreviewTrail = false; synchronized (mGesturePreviewTrails) { // Trails count == fingers count that have ever been active. @@ -209,9 +239,18 @@ public class PreviewPlacerView extends RelativeLayout { for (int index = 0; index < trailsCount; index++) { final GesturePreviewTrail trail = mGesturePreviewTrails.valueAt(index); needsUpdatingGesturePreviewTrail |= - trail.drawGestureTrail(canvas, mGesturePaint); + trail.drawGestureTrail(mOffscreenCanvas, mGesturePaint, + mGesturePreviewTrailBoundsRect, mGesturePreviewTrailParams); + // {@link #mGesturePreviewTrailBoundsRect} has bounding box of the trail. + mOffscreenDirtyRect.union(mGesturePreviewTrailBoundsRect); } } + if (!mOffscreenDirtyRect.isEmpty()) { + canvas.drawBitmap(mOffscreenBuffer, mOffscreenDirtyRect, mOffscreenDirtyRect, + mGesturePaint); + // Note: Defer clearing the dirty rectangle here because we will get cleared + // rectangle on the canvas. + } if (needsUpdatingGesturePreviewTrail) { mDrawingHandler.postUpdateGestureTrailPreview(); } @@ -242,45 +281,29 @@ public class PreviewPlacerView extends RelativeLayout { } final Paint paint = mTextPaint; + final RectF rectangle = mGestureFloatingPreviewRectangle; // 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(); - - final int halfTextWidth = (int)paint.measureText(gestureFloatingPreviewText) / 2 + textSize; - final int textX = Math.min(Math.max(lastX, halfTextWidth), canvasWidth - halfTextWidth); - - int textY = Math.max(-textSize, lastY - mGestureFloatingPreviewTextOffset); - if (textY < 0) { - // Paint black text shadow if preview extends above keyboard region. - paint.setStyle(Paint.Style.FILL_AND_STROKE); - paint.setColor(mGestureFloatingPreviewTextShadowColor); - paint.setStrokeWidth(mGestureFloatingPreviewTextShadowBorder); - canvas.drawText(gestureFloatingPreviewText, textX, textY, paint); - } - - // Paint the vertical line connecting the touch point to the preview text. - paint.setStyle(Paint.Style.STROKE); - paint.setColor(mGestureFloatingPreviewTextConnectorColor); - paint.setStrokeWidth(mGestureFloatingPreviewTextConnectorWidth); - final int lineTopY = textY - textSize / 4; - canvas.drawLine(lastX, lastY, lastX, lineTopY, paint); - if (lastX != textX) { - // Paint the horizontal line connection the touch point to the preview text. - canvas.drawLine(lastX, lineTopY, textX, lineTopY, paint); - } - - // Paint the shading for the text preview - paint.setStyle(Paint.Style.FILL_AND_STROKE); - paint.setColor(mGestureFloatingPreviewTextShadingColor); - paint.setStrokeWidth(mGestureFloatingPreviewTextShadingBorder); - canvas.drawText(gestureFloatingPreviewText, textX, textY, paint); + // Paint the round rectangle background. + final int textHeight = mGestureFloatingPreviewTextHeight; + final float textWidth = paint.measureText(gestureFloatingPreviewText); + final float hPad = mGestureFloatingPreviewHorizontalPadding; + final float vPad = mGestureFloatingPreviewVerticalPadding; + final float rectWidth = textWidth + hPad * 2.0f; + final float rectHeight = textHeight + vPad * 2.0f; + final int canvasWidth = canvas.getWidth(); + final float rectX = Math.min(Math.max(mLastPointerX - rectWidth / 2.0f, 0.0f), + canvasWidth - rectWidth); + final float rectY = mLastPointerY - mGestureFloatingPreviewTextOffset - rectHeight; + rectangle.set(rectX, rectY, rectX + rectWidth, rectY + rectHeight); + final float round = mGestureFloatingPreviewRoundRadius; + paint.setColor(mGestureFloatingPreviewColor); + canvas.drawRoundRect(rectangle, round, round, paint); // Paint the text preview paint.setColor(mGestureFloatingPreviewTextColor); - paint.setStyle(Paint.Style.FILL); + final float textX = rectX + hPad + textWidth / 2.0f; + final float textY = rectY + vPad + textHeight; canvas.drawText(gestureFloatingPreviewText, textX, textY, paint); } } 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 4b47a261f..509fc1ba3 100644 --- a/java/src/com/android/inputmethod/latin/AdditionalSubtype.java +++ b/java/src/com/android/inputmethod/latin/AdditionalSubtype.java @@ -27,22 +27,22 @@ import android.view.inputmethod.InputMethodSubtype; import java.util.ArrayList; -public class AdditionalSubtype { +public final class AdditionalSubtype { private static final InputMethodSubtype[] EMPTY_SUBTYPE_ARRAY = new InputMethodSubtype[0]; private AdditionalSubtype() { // This utility class is not publicly instantiable. } - public static boolean isAdditionalSubtype(InputMethodSubtype subtype) { + public static boolean isAdditionalSubtype(final InputMethodSubtype subtype) { return subtype.containsExtraValueKey(IS_ADDITIONAL_SUBTYPE); } private static final String LOCALE_AND_LAYOUT_SEPARATOR = ":"; - public static final String PREF_SUBTYPE_SEPARATOR = ";"; + private static final String PREF_SUBTYPE_SEPARATOR = ";"; - public static InputMethodSubtype createAdditionalSubtype( - String localeString, String keyboardLayoutSetName, String extraValue) { + public static InputMethodSubtype createAdditionalSubtype(final String localeString, + final String keyboardLayoutSetName, final String extraValue) { final String layoutExtraValue = KEYBOARD_LAYOUT_SET + "=" + keyboardLayoutSetName; final String layoutDisplayNameExtraValue; if (Build.VERSION.SDK_INT >= /* JELLY_BEAN */ 15 @@ -62,7 +62,7 @@ public class AdditionalSubtype { layoutExtraValue + "," + additionalSubtypeExtraValue, false, false); } - public static String getPrefSubtype(InputMethodSubtype subtype) { + public static String getPrefSubtype(final InputMethodSubtype subtype) { final String localeString = subtype.getLocale(); final String keyboardLayoutSetName = SubtypeLocale.getKeyboardLayoutSetName(subtype); final String layoutExtraValue = KEYBOARD_LAYOUT_SET + "=" + keyboardLayoutSetName; @@ -74,7 +74,7 @@ public class AdditionalSubtype { : basePrefSubtype + LOCALE_AND_LAYOUT_SEPARATOR + extraValue; } - public static InputMethodSubtype createAdditionalSubtype(String prefSubtype) { + public static InputMethodSubtype createAdditionalSubtype(final String prefSubtype) { final String elems[] = prefSubtype.split(LOCALE_AND_LAYOUT_SEPARATOR); if (elems.length < 2 || elems.length > 3) { throw new RuntimeException("Unknown additional subtype specified: " + prefSubtype); @@ -85,7 +85,7 @@ public class AdditionalSubtype { return createAdditionalSubtype(localeString, keyboardLayoutSetName, extraValue); } - public static InputMethodSubtype[] createAdditionalSubtypesArray(String prefSubtypes) { + public static InputMethodSubtype[] createAdditionalSubtypesArray(final String prefSubtypes) { if (TextUtils.isEmpty(prefSubtypes)) { return EMPTY_SUBTYPE_ARRAY; } @@ -103,4 +103,32 @@ public class AdditionalSubtype { } return subtypesList.toArray(new InputMethodSubtype[subtypesList.size()]); } + + public static String createPrefSubtypes(final InputMethodSubtype[] subtypes) { + if (subtypes == null || subtypes.length == 0) { + return ""; + } + final StringBuilder sb = new StringBuilder(); + for (final InputMethodSubtype subtype : subtypes) { + if (sb.length() > 0) { + sb.append(PREF_SUBTYPE_SEPARATOR); + } + sb.append(getPrefSubtype(subtype)); + } + return sb.toString(); + } + + public static String createPrefSubtypes(final String[] prefSubtypes) { + if (prefSubtypes == null || prefSubtypes.length == 0) { + return ""; + } + final StringBuilder sb = new StringBuilder(); + for (final String prefSubtype : prefSubtypes) { + if (sb.length() > 0) { + sb.append(PREF_SUBTYPE_SEPARATOR); + } + sb.append(prefSubtype); + } + return sb.toString(); + } } diff --git a/java/src/com/android/inputmethod/latin/AdditionalSubtypeSettings.java b/java/src/com/android/inputmethod/latin/AdditionalSubtypeSettings.java index d01592a4d..ae51d2537 100644 --- a/java/src/com/android/inputmethod/latin/AdditionalSubtypeSettings.java +++ b/java/src/com/android/inputmethod/latin/AdditionalSubtypeSettings.java @@ -63,13 +63,13 @@ public class AdditionalSubtypeSettings extends PreferenceFragment { private static final String KEY_IS_SUBTYPE_ENABLER_NOTIFICATION_DIALOG_OPEN = "is_subtype_enabler_notification_dialog_open"; private static final String KEY_SUBTYPE_FOR_SUBTYPE_ENABLER = "subtype_for_subtype_enabler"; - static class SubtypeLocaleItem extends Pair<String, String> + static final class SubtypeLocaleItem extends Pair<String, String> implements Comparable<SubtypeLocaleItem> { - public SubtypeLocaleItem(String localeString, String displayName) { + public SubtypeLocaleItem(final String localeString, final String displayName) { super(localeString, displayName); } - public SubtypeLocaleItem(String localeString) { + public SubtypeLocaleItem(final String localeString) { this(localeString, SubtypeLocale.getSubtypeLocaleDisplayName(localeString)); } @@ -79,13 +79,13 @@ public class AdditionalSubtypeSettings extends PreferenceFragment { } @Override - public int compareTo(SubtypeLocaleItem o) { + public int compareTo(final SubtypeLocaleItem o) { return first.compareTo(o.first); } } - static class SubtypeLocaleAdapter extends ArrayAdapter<SubtypeLocaleItem> { - public SubtypeLocaleAdapter(Context context) { + static final class SubtypeLocaleAdapter extends ArrayAdapter<SubtypeLocaleItem> { + public SubtypeLocaleAdapter(final Context context) { super(context, android.R.layout.simple_spinner_item); setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); @@ -102,7 +102,8 @@ public class AdditionalSubtypeSettings extends PreferenceFragment { addAll(items); } - public static SubtypeLocaleItem createItem(Context context, String localeString) { + public static SubtypeLocaleItem createItem(final Context context, + final String localeString) { if (localeString.equals(SubtypeLocale.NO_LANGUAGE)) { final String displayName = context.getString(R.string.subtype_no_language); return new SubtypeLocaleItem(localeString, displayName); @@ -112,8 +113,8 @@ public class AdditionalSubtypeSettings extends PreferenceFragment { } } - static class KeyboardLayoutSetItem extends Pair<String, String> { - public KeyboardLayoutSetItem(InputMethodSubtype subtype) { + static final class KeyboardLayoutSetItem extends Pair<String, String> { + public KeyboardLayoutSetItem(final InputMethodSubtype subtype) { super(SubtypeLocale.getKeyboardLayoutSetName(subtype), SubtypeLocale.getKeyboardLayoutSetDisplayName(subtype)); } @@ -124,8 +125,8 @@ public class AdditionalSubtypeSettings extends PreferenceFragment { } } - static class KeyboardLayoutSetAdapter extends ArrayAdapter<KeyboardLayoutSetItem> { - public KeyboardLayoutSetAdapter(Context context) { + static final class KeyboardLayoutSetAdapter extends ArrayAdapter<KeyboardLayoutSetItem> { + public KeyboardLayoutSetAdapter(final Context context) { super(context, android.R.layout.simple_spinner_item); setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); @@ -147,7 +148,7 @@ public class AdditionalSubtypeSettings extends PreferenceFragment { public KeyboardLayoutSetAdapter getKeyboardLayoutSetAdapter(); } - static class SubtypePreference extends DialogPreference + static final class SubtypePreference extends DialogPreference implements DialogInterface.OnCancelListener { private static final String KEY_PREFIX = "subtype_pref_"; private static final String KEY_NEW_SUBTYPE = KEY_PREFIX + "new"; @@ -159,13 +160,13 @@ public class AdditionalSubtypeSettings extends PreferenceFragment { private Spinner mSubtypeLocaleSpinner; private Spinner mKeyboardLayoutSetSpinner; - public static SubtypePreference newIncompleteSubtypePreference( - Context context, SubtypeDialogProxy proxy) { + public static SubtypePreference newIncompleteSubtypePreference(final Context context, + final SubtypeDialogProxy proxy) { return new SubtypePreference(context, null, proxy); } - public SubtypePreference(Context context, InputMethodSubtype subtype, - SubtypeDialogProxy proxy) { + public SubtypePreference(final Context context, final InputMethodSubtype subtype, + final SubtypeDialogProxy proxy) { super(context, null); setDialogLayoutResource(R.layout.additional_subtype_dialog); setPersistent(false); @@ -185,7 +186,7 @@ public class AdditionalSubtypeSettings extends PreferenceFragment { return mSubtype; } - public void setSubtype(InputMethodSubtype subtype) { + public void setSubtype(final InputMethodSubtype subtype) { mPreviousSubtype = mSubtype; mSubtype = subtype; if (isIncomplete()) { @@ -221,7 +222,7 @@ public class AdditionalSubtypeSettings extends PreferenceFragment { } @Override - protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { + protected void onPrepareDialogBuilder(final AlertDialog.Builder builder) { final Context context = builder.getContext(); builder.setCancelable(true).setOnCancelListener(this); if (isIncomplete()) { @@ -239,7 +240,7 @@ public class AdditionalSubtypeSettings extends PreferenceFragment { } } - private static void setSpinnerPosition(Spinner spinner, Object itemToSelect) { + private static void setSpinnerPosition(final Spinner spinner, final Object itemToSelect) { final SpinnerAdapter adapter = spinner.getAdapter(); final int count = adapter.getCount(); for (int i = 0; i < count; i++) { @@ -252,14 +253,14 @@ public class AdditionalSubtypeSettings extends PreferenceFragment { } @Override - public void onCancel(DialogInterface dialog) { + public void onCancel(final DialogInterface dialog) { if (isIncomplete()) { mProxy.onRemovePressed(this); } } @Override - public void onClick(DialogInterface dialog, int which) { + public void onClick(final DialogInterface dialog, final int which) { super.onClick(dialog, which); switch (which) { case DialogInterface.BUTTON_POSITIVE: @@ -287,12 +288,12 @@ public class AdditionalSubtypeSettings extends PreferenceFragment { } } - private static int getSpinnerPosition(Spinner spinner) { + private static int getSpinnerPosition(final Spinner spinner) { if (spinner == null) return -1; return spinner.getSelectedItemPosition(); } - private static void setSpinnerPosition(Spinner spinner, int position) { + private static void setSpinnerPosition(final Spinner spinner, final int position) { if (spinner == null || position < 0) return; spinner.setSelection(position); } @@ -313,7 +314,7 @@ public class AdditionalSubtypeSettings extends PreferenceFragment { } @Override - protected void onRestoreInstanceState(Parcelable state) { + protected void onRestoreInstanceState(final Parcelable state) { if (!(state instanceof SavedState)) { super.onRestoreInstanceState(state); return; @@ -326,24 +327,24 @@ public class AdditionalSubtypeSettings extends PreferenceFragment { setSubtype(myState.mSubtype); } - static class SavedState extends Preference.BaseSavedState { + static final class SavedState extends Preference.BaseSavedState { InputMethodSubtype mSubtype; int mSubtypeLocaleSelectedPos; int mKeyboardLayoutSetSelectedPos; - public SavedState(Parcelable superState) { + public SavedState(final Parcelable superState) { super(superState); } @Override - public void writeToParcel(Parcel dest, int flags) { + public void writeToParcel(final Parcel dest, final int flags) { super.writeToParcel(dest, flags); dest.writeInt(mSubtypeLocaleSelectedPos); dest.writeInt(mKeyboardLayoutSetSelectedPos); dest.writeParcelable(mSubtype, 0); } - public SavedState(Parcel source) { + public SavedState(final Parcel source) { super(source); mSubtypeLocaleSelectedPos = source.readInt(); mKeyboardLayoutSetSelectedPos = source.readInt(); @@ -354,12 +355,12 @@ public class AdditionalSubtypeSettings extends PreferenceFragment { public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() { @Override - public SavedState createFromParcel(Parcel source) { + public SavedState createFromParcel(final Parcel source) { return new SavedState(source); } @Override - public SavedState[] newArray(int size) { + public SavedState[] newArray(final int size) { return new SavedState[size]; } }; @@ -371,7 +372,7 @@ public class AdditionalSubtypeSettings extends PreferenceFragment { } @Override - public void onCreate(Bundle savedInstanceState) { + public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); addPreferencesFromResource(R.xml.additional_subtype_settings); @@ -381,7 +382,7 @@ public class AdditionalSubtypeSettings extends PreferenceFragment { } @Override - public void onActivityCreated(Bundle savedInstanceState) { + public void onActivityCreated(final Bundle savedInstanceState) { final Context context = getActivity(); mSubtypeLocaleAdapter = new SubtypeLocaleAdapter(context); mKeyboardLayoutSetAdapter = new KeyboardLayoutSetAdapter(context); @@ -411,7 +412,7 @@ public class AdditionalSubtypeSettings extends PreferenceFragment { } @Override - public void onSaveInstanceState(Bundle outState) { + public void onSaveInstanceState(final Bundle outState) { super.onSaveInstanceState(outState); if (mIsAddingNewSubtype) { outState.putBoolean(KEY_IS_ADDING_NEW_SUBTYPE, true); @@ -426,7 +427,7 @@ public class AdditionalSubtypeSettings extends PreferenceFragment { private final SubtypeDialogProxy mSubtypeProxy = new SubtypeDialogProxy() { @Override - public void onRemovePressed(SubtypePreference subtypePref) { + public void onRemovePressed(final SubtypePreference subtypePref) { mIsAddingNewSubtype = false; final PreferenceGroup group = getPreferenceScreen(); group.removePreference(subtypePref); @@ -434,7 +435,7 @@ public class AdditionalSubtypeSettings extends PreferenceFragment { } @Override - public void onSavePressed(SubtypePreference subtypePref) { + public void onSavePressed(final SubtypePreference subtypePref) { final InputMethodSubtype subtype = subtypePref.getSubtype(); if (!subtypePref.hasBeenModified()) { return; @@ -453,7 +454,7 @@ public class AdditionalSubtypeSettings extends PreferenceFragment { } @Override - public void onAddPressed(SubtypePreference subtypePref) { + public void onAddPressed(final SubtypePreference subtypePref) { mIsAddingNewSubtype = false; final InputMethodSubtype subtype = subtypePref.getSubtype(); if (findDuplicatedSubtype(subtype) == null) { @@ -481,7 +482,7 @@ public class AdditionalSubtypeSettings extends PreferenceFragment { } }; - private void showSubtypeAlreadyExistsToast(InputMethodSubtype subtype) { + private void showSubtypeAlreadyExistsToast(final InputMethodSubtype subtype) { final Context context = getActivity(); final Resources res = context.getResources(); final String message = res.getString(R.string.custom_input_style_already_exists, @@ -489,14 +490,15 @@ public class AdditionalSubtypeSettings extends PreferenceFragment { Toast.makeText(context, message, Toast.LENGTH_SHORT).show(); } - private InputMethodSubtype findDuplicatedSubtype(InputMethodSubtype subtype) { + private InputMethodSubtype findDuplicatedSubtype(final InputMethodSubtype subtype) { final String localeString = subtype.getLocale(); final String keyboardLayoutSetName = SubtypeLocale.getKeyboardLayoutSetName(subtype); return ImfUtils.findSubtypeByLocaleAndKeyboardLayoutSet( getActivity(), localeString, keyboardLayoutSetName); } - private AlertDialog createDialog(SubtypePreference subtypePref) { + private AlertDialog createDialog( + @SuppressWarnings("unused") final SubtypePreference subtypePref) { final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); builder.setTitle(R.string.custom_input_styles_title) .setMessage(R.string.custom_input_style_note_message) @@ -519,7 +521,7 @@ public class AdditionalSubtypeSettings extends PreferenceFragment { return builder.create(); } - private void setPrefSubtypes(String prefSubtypes, Context context) { + private void setPrefSubtypes(final String prefSubtypes, final Context context) { final PreferenceGroup group = getPreferenceScreen(); group.removeAll(); final InputMethodSubtype[] subtypesArray = @@ -547,23 +549,12 @@ public class AdditionalSubtypeSettings extends PreferenceFragment { return subtypes.toArray(new InputMethodSubtype[subtypes.size()]); } - private String getPrefSubtypes(InputMethodSubtype[] subtypes) { - final StringBuilder sb = new StringBuilder(); - for (final InputMethodSubtype subtype : subtypes) { - if (sb.length() > 0) { - sb.append(AdditionalSubtype.PREF_SUBTYPE_SEPARATOR); - } - sb.append(AdditionalSubtype.getPrefSubtype(subtype)); - } - return sb.toString(); - } - @Override public void onPause() { super.onPause(); final String oldSubtypes = SettingsValues.getPrefAdditionalSubtypes(mPrefs, getResources()); final InputMethodSubtype[] subtypes = getSubtypes(); - final String prefSubtypes = getPrefSubtypes(subtypes); + final String prefSubtypes = AdditionalSubtype.createPrefSubtypes(subtypes); if (prefSubtypes.equals(oldSubtypes)) { return; } @@ -578,13 +569,13 @@ public class AdditionalSubtypeSettings extends PreferenceFragment { } @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { final MenuItem addSubtypeMenu = menu.add(0, MENU_ADD_SUBTYPE, 0, R.string.add_style); addSubtypeMenu.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); } @Override - public boolean onOptionsItemSelected(MenuItem item) { + public boolean onOptionsItemSelected(final MenuItem item) { final int itemId = item.getItemId(); if (itemId == MENU_ADD_SUBTYPE) { final SubtypePreference newSubtype = diff --git a/java/src/com/android/inputmethod/latin/AutoCorrection.java b/java/src/com/android/inputmethod/latin/AutoCorrection.java index 01ba30077..f425e360a 100644 --- a/java/src/com/android/inputmethod/latin/AutoCorrection.java +++ b/java/src/com/android/inputmethod/latin/AutoCorrection.java @@ -73,11 +73,11 @@ public class AutoCorrection { return maxFreq; } - // Returns true if this isn't in any dictionary. - public static boolean isNotAWord( + // Returns true if this is in any of the dictionaries. + public static boolean isInTheDictionary( final ConcurrentHashMap<String, Dictionary> dictionaries, final CharSequence word, final boolean ignoreCase) { - return !isValidWord(dictionaries, word, ignoreCase); + return isValidWord(dictionaries, word, ignoreCase); } public static boolean suggestionExceedsAutoCorrectionThreshold(SuggestedWordInfo suggestion, diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java index 8909526d8..c3ae81f3a 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java @@ -41,7 +41,7 @@ public class BinaryDictionary extends Dictionary { * It is necessary to keep it at this value because some languages e.g. German have * really long words. */ - public static final int MAX_WORD_LENGTH = 48; + public static final int MAX_WORD_LENGTH = Constants.Dictionary.MAX_WORD_LENGTH; public static final int MAX_WORDS = 18; public static final int MAX_SPACES = 16; diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java index e1cb195bc..9a888ade4 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java @@ -17,6 +17,7 @@ package com.android.inputmethod.latin; import com.android.inputmethod.latin.makedict.BinaryDictInputOutput; +import com.android.inputmethod.latin.makedict.FormatSpec; import android.content.Context; import android.content.SharedPreferences; @@ -359,7 +360,7 @@ class BinaryDictionaryGetter { 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) { + if (magic != FormatSpec.VERSION_2_MAGIC_NUMBER) { return false; } final int formatVersion = buffer.getInt(); diff --git a/java/src/com/android/inputmethod/latin/CollectionUtils.java b/java/src/com/android/inputmethod/latin/CollectionUtils.java index baa2ee1cd..c75f2df5c 100644 --- a/java/src/com/android/inputmethod/latin/CollectionUtils.java +++ b/java/src/com/android/inputmethod/latin/CollectionUtils.java @@ -30,7 +30,7 @@ import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; -public class CollectionUtils { +public final class CollectionUtils { private CollectionUtils() { // This utility class is not publicly instantiable. } diff --git a/java/src/com/android/inputmethod/latin/Constants.java b/java/src/com/android/inputmethod/latin/Constants.java index d71c0f995..57e12a64f 100644 --- a/java/src/com/android/inputmethod/latin/Constants.java +++ b/java/src/com/android/inputmethod/latin/Constants.java @@ -16,8 +16,6 @@ package com.android.inputmethod.latin; -import android.view.inputmethod.EditorInfo; - public final class Constants { public static final class Color { /** @@ -54,7 +52,7 @@ public final class Constants { * The private IME option used to indicate that the given text field needs ASCII code points * input. * - * @deprecated Use {@link EditorInfo#IME_FLAG_FORCE_ASCII}. + * @deprecated Use EditorInfo#IME_FLAG_FORCE_ASCII. */ @SuppressWarnings("dep-ann") public static final String FORCE_ASCII = "forceAscii"; @@ -128,6 +126,14 @@ public final class Constants { } } + public static class Dictionary { + public static final int MAX_WORD_LENGTH = 48; + + private Dictionary() { + // This utility class is no publicly instantiable. + } + } + public static final int NOT_A_CODE = -1; // See {@link KeyboardActionListener.Adapter#isInvalidCoordinate(int)}. diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java index cdf5247de..b93c17f11 100644 --- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java @@ -21,6 +21,7 @@ import android.util.Log; import com.android.inputmethod.keyboard.ProximityInfo; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.makedict.BinaryDictInputOutput; +import com.android.inputmethod.latin.makedict.FormatSpec; import com.android.inputmethod.latin.makedict.FusionDictionary; import com.android.inputmethod.latin.makedict.FusionDictionary.Node; import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString; @@ -89,6 +90,10 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { /** Controls access to the local binary dictionary for this instance. */ private final DictionaryController mLocalDictionaryController = new DictionaryController(); + private static final int BINARY_DICT_VERSION = 1; + private static final FormatSpec.FormatOptions FORMAT_OPTIONS = + new FormatSpec.FormatOptions(BINARY_DICT_VERSION); + /** * Abstract method for loading the unigrams and bigrams of a given dictionary in a background * thread. @@ -172,12 +177,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 = CollectionUtils.newArrayList(); shortcutTargets.add(new WeightedString(shortcutTarget, frequency)); - mFusionDictionary.add(word, frequency, shortcutTargets); + mFusionDictionary.add(word, frequency, shortcutTargets, false /* isNotAWord */); } } @@ -310,7 +315,7 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { FileOutputStream out = null; try { out = new FileOutputStream(tempFile); - BinaryDictInputOutput.writeDictionaryBinary(out, mFusionDictionary, 1); + BinaryDictInputOutput.writeDictionaryBinary(out, mFusionDictionary, FORMAT_OPTIONS); out.flush(); out.close(); tempFile.renameTo(file); 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/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 83a306818..db8f269eb 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -36,6 +36,8 @@ import android.inputmethodservice.InputMethodService; import android.media.AudioManager; import android.net.ConnectivityManager; import android.os.Debug; +import android.os.Handler; +import android.os.HandlerThread; import android.os.IBinder; import android.os.Message; import android.os.SystemClock; @@ -184,13 +186,16 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private static final int MSG_UPDATE_SHIFT_STATE = 0; private static final int MSG_PENDING_IMS_CALLBACK = 1; private static final int MSG_UPDATE_SUGGESTION_STRIP = 2; + private static final int MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP = 3; + + private static final int ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT = 1; private int mDelayUpdateSuggestions; private int mDelayUpdateShiftState; private long mDoubleSpacesTurnIntoPeriodTimeout; private long mDoubleSpaceTimerStart; - public UIHandler(LatinIME outerInstance) { + public UIHandler(final LatinIME outerInstance) { super(outerInstance); } @@ -205,7 +210,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } @Override - public void handleMessage(Message msg) { + public void handleMessage(final Message msg) { final LatinIME latinIme = getOuterInstance(); final KeyboardSwitcher switcher = latinIme.mKeyboardSwitcher; switch (msg.what) { @@ -215,6 +220,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen case MSG_UPDATE_SHIFT_STATE: switcher.updateShiftState(); break; + case MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP: + latinIme.showGesturePreviewAndSuggestionStrip((SuggestedWords)msg.obj, + msg.arg1 == ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT); + break; } } @@ -239,6 +248,15 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen removeMessages(MSG_UPDATE_SHIFT_STATE); } + public void showGesturePreviewAndSuggestionStrip(final SuggestedWords suggestedWords, + final boolean dismissGestureFloatingPreviewText) { + removeMessages(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP); + final int arg1 = dismissGestureFloatingPreviewText + ? ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT : 0; + obtainMessage(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, arg1, 0, suggestedWords) + .sendToTarget(); + } + public void startDoubleSpacesTimer() { mDoubleSpaceTimerStart = SystemClock.uptimeMillis(); } @@ -276,7 +294,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mHasPendingStartInput = false; } - private void executePendingImsCallback(LatinIME latinIme, EditorInfo editorInfo, + private void executePendingImsCallback(final LatinIME latinIme, final EditorInfo editorInfo, boolean restarting) { if (mHasPendingFinishInputView) latinIme.onFinishInputViewInternal(mHasPendingFinishInput); @@ -287,7 +305,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen resetPendingImsCallback(); } - public void onStartInput(EditorInfo editorInfo, boolean restarting) { + public void onStartInput(final EditorInfo editorInfo, final boolean restarting) { if (hasMessages(MSG_PENDING_IMS_CALLBACK)) { // Typically this is the second onStartInput after orientation changed. mHasPendingStartInput = true; @@ -303,7 +321,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } } - public void onStartInputView(EditorInfo editorInfo, boolean restarting) { + public void onStartInputView(final EditorInfo editorInfo, final boolean restarting) { if (hasMessages(MSG_PENDING_IMS_CALLBACK) && KeyboardId.equivalentEditorInfoForKeyboard(editorInfo, mAppliedEditorInfo)) { // Typically this is the second onStartInputView after orientation changed. @@ -323,7 +341,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } } - public void onFinishInputView(boolean finishingInput) { + public void onFinishInputView(final boolean finishingInput) { if (hasMessages(MSG_PENDING_IMS_CALLBACK)) { // Typically this is the first onFinishInputView after orientation changed. mHasPendingFinishInputView = true; @@ -425,7 +443,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // Note that this method is called from a non-UI thread. @Override - public void onUpdateMainDictionaryAvailability(boolean isMainDictionaryAvailable) { + public void onUpdateMainDictionaryAvailability(final boolean isMainDictionaryAvailable) { mIsMainDictionaryAvailable = isMainDictionaryAvailable; final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); if (mainKeyboardView != null) { @@ -529,7 +547,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } @Override - public void onConfigurationChanged(Configuration conf) { + public void onConfigurationChanged(final Configuration conf) { // System locale has been changed. Needs to reload keyboard. if (mSubtypeSwitcher.onConfigurationChanged(conf, this)) { loadKeyboard(); @@ -555,7 +573,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } @Override - public void setInputView(View view) { + public void setInputView(final View view) { super.setInputView(view); mExtractArea = getWindow().getWindow().getDecorView() .findViewById(android.R.id.extractArea); @@ -570,23 +588,23 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } @Override - public void setCandidatesView(View view) { + public void setCandidatesView(final View view) { // To ensure that CandidatesView will never be set. return; } @Override - public void onStartInput(EditorInfo editorInfo, boolean restarting) { + public void onStartInput(final EditorInfo editorInfo, final boolean restarting) { mHandler.onStartInput(editorInfo, restarting); } @Override - public void onStartInputView(EditorInfo editorInfo, boolean restarting) { + public void onStartInputView(final EditorInfo editorInfo, final boolean restarting) { mHandler.onStartInputView(editorInfo, restarting); } @Override - public void onFinishInputView(boolean finishingInput) { + public void onFinishInputView(final boolean finishingInput) { mHandler.onFinishInputView(finishingInput); } @@ -596,19 +614,19 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } @Override - public void onCurrentInputMethodSubtypeChanged(InputMethodSubtype subtype) { + public void onCurrentInputMethodSubtypeChanged(final InputMethodSubtype subtype) { // 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) { + private void onStartInputInternal(final EditorInfo editorInfo, final boolean restarting) { super.onStartInput(editorInfo, restarting); } @SuppressWarnings("deprecation") - private void onStartInputViewInternal(EditorInfo editorInfo, boolean restarting) { + private void onStartInputViewInternal(final EditorInfo editorInfo, final boolean restarting) { super.onStartInputView(editorInfo, restarting); final KeyboardSwitcher switcher = mKeyboardSwitcher; final MainKeyboardView mainKeyboardView = switcher.getMainKeyboardView(); @@ -700,6 +718,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } } + mConnection.resetCachesUponCursorMove(mLastSelectionStart); + if (isDifferentTextField) { mainKeyboardView.closing(); loadSettings(); @@ -749,7 +769,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen getCurrentInputConnection()); } super.onWindowHidden(); - final KeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); + final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); if (mainKeyboardView != null) { mainKeyboardView.closing(); } @@ -763,16 +783,16 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen ResearchLogger.getInstance().latinIME_onFinishInputInternal(); } - final KeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); + final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); if (mainKeyboardView != null) { mainKeyboardView.closing(); } } - private void onFinishInputViewInternal(boolean finishingInput) { + private void onFinishInputViewInternal(final boolean finishingInput) { super.onFinishInputView(finishingInput); mKeyboardSwitcher.onFinishInputView(); - final KeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); + final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); if (mainKeyboardView != null) { mainKeyboardView.cancelAllMessages(); } @@ -781,9 +801,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } @Override - public void onUpdateSelection(int oldSelStart, int oldSelEnd, - int newSelStart, int newSelEnd, - int composingSpanStart, int composingSpanEnd) { + public void onUpdateSelection(final int oldSelStart, final int oldSelEnd, + final int newSelStart, final int newSelEnd, + final int composingSpanStart, final int composingSpanEnd) { super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, composingSpanStart, composingSpanEnd); if (DEBUG) { @@ -823,7 +843,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // we know for sure the cursor moved while we were composing and we should reset // the state. final boolean noComposingSpan = composingSpanStart == -1 && composingSpanEnd == -1; - if (!mExpectingUpdateSelection) { + if (!mExpectingUpdateSelection + && !mConnection.isBelatedExpectedUpdate(oldSelStart, newSelStart)) { // TAKE CARE: there is a race condition when we enter this test even when the user // did not explicitly move the cursor. This happens when typing fast, where two keys // turn this flag on in succession and both onUpdateSelection() calls arrive after @@ -839,7 +860,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mSpaceState = SPACE_STATE_NONE; if ((!mWordComposer.isComposingWord()) || selectionChanged || noComposingSpan) { - resetEntireInputState(); + resetEntireInputState(newSelStart); } mHandler.postUpdateShiftState(); @@ -880,7 +901,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen * cause the suggestions strip to disappear and re-appear. */ @Override - public void onExtractedCursorMovement(int dx, int dy) { + public void onExtractedCursorMovement(final int dx, final int dy) { if (mCurrentSettings.isSuggestionsRequested(mDisplayOrientation)) return; super.onExtractedCursorMovement(dx, dy); @@ -900,7 +921,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } @Override - public void onDisplayCompletions(CompletionInfo[] applicationSpecifiedCompletions) { + public void onDisplayCompletions(final CompletionInfo[] applicationSpecifiedCompletions) { if (DEBUG) { Log.i(TAG, "Received completions:"); if (applicationSpecifiedCompletions != null) { @@ -942,7 +963,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } } - private void setSuggestionStripShownInternal(boolean shown, boolean needsInputViewShown) { + private void setSuggestionStripShownInternal(final boolean shown, + final boolean needsInputViewShown) { // TODO: Modify this if we support suggestions with hard keyboard if (onEvaluateInputViewShown() && mSuggestionsContainer != null) { final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); @@ -960,7 +982,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } } - private void setSuggestionStripShown(boolean shown) { + private void setSuggestionStripShown(final boolean shown) { setSuggestionStripShownInternal(shown, /* needsInputViewShown */true); } @@ -970,7 +992,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen return currentHeight; } - final KeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); + final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); if (mainKeyboardView == null) { return 0; } @@ -990,9 +1012,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } @Override - public void onComputeInsets(InputMethodService.Insets outInsets) { + public void onComputeInsets(final InputMethodService.Insets outInsets) { super.onComputeInsets(outInsets); - final KeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); + final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); if (mainKeyboardView == null || mSuggestionsContainer == null) { return; } @@ -1043,10 +1065,14 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // This will reset the whole input state to the starting state. It will clear // the composing word, reset the last composed word, tell the inputconnection about it. - private void resetEntireInputState() { + private void resetEntireInputState(final int newCursorPosition) { resetComposingState(true /* alsoResetLastComposedWord */); - clearSuggestionStrip(); - mConnection.finishComposingText(); + if (mCurrentSettings.mBigramPredictionEnabled) { + clearSuggestionStrip(); + } else { + setSuggestionStrip(mCurrentSettings.mSuggestPuncList, false); + } + mConnection.resetCachesUponCursorMove(newCursorPosition); } private void resetComposingState(final boolean alsoResetLastComposedWord) { @@ -1055,7 +1081,7 @@ 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) { @@ -1063,7 +1089,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen final CharSequence prevWord = addToUserHistoryDictionary(typedWord); mLastComposedWord = mWordComposer.commitWord( LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD, typedWord.toString(), - separatorCode, prevWord); + separatorString, prevWord); } } @@ -1092,7 +1118,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // Note: getCursorCapsMode() returns the current capitalization mode that is any // combination of CAP_MODE_CHARACTERS, CAP_MODE_WORDS, and CAP_MODE_SENTENCES. 0 means none // of them. - return mConnection.getCursorCapsMode(inputType); + return mConnection.getCursorCapsMode(inputType, mSubtypeSwitcher.getCurrentSubtypeLocale()); } // Factor in auto-caps and manual caps and compute the current caps mode. @@ -1153,12 +1179,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // Callback for the {@link SuggestionStripView}, to call when the "add to dictionary" hint is // pressed. @Override - public boolean addWordToUserDictionary(String word) { + public boolean addWordToUserDictionary(final String word) { mUserDictionary.addWordToUserDictionary(word, 128); return true; } - private static boolean isAlphabet(int code) { + private static boolean isAlphabet(final int code) { return Character.isLetter(code); } @@ -1171,7 +1197,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen public static final int CODE_SHOW_INPUT_METHOD_PICKER = 1; @Override - public boolean onCustomRequest(int requestCode) { + public boolean onCustomRequest(final int requestCode) { if (isShowingOptionDialog()) return false; switch (requestCode) { case CODE_SHOW_INPUT_METHOD_PICKER: @@ -1189,11 +1215,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen return mOptionsDialog != null && mOptionsDialog.isShowing(); } - private static int getActionId(Keyboard keyboard) { + private static int getActionId(final Keyboard keyboard) { return keyboard != null ? keyboard.mId.imeActionId() : EditorInfo.IME_ACTION_NONE; } - private void performEditorAction(int actionId) { + private void performEditorAction(final int actionId) { mConnection.performEditorAction(actionId); } @@ -1216,7 +1242,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } } - private void sendUpDownEnterOrBackspace(final int code) { + private void sendDownUpKeyEventForBackwardCompatibility(final int code) { final long eventTime = SystemClock.uptimeMillis(); mConnection.sendKeyEvent(new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, code, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, @@ -1226,11 +1252,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE)); } - private void sendKeyCodePoint(int code) { + private void sendKeyCodePoint(final int code) { // TODO: Remove this special handling of digit letters. // For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}. if (code >= '0' && code <= '9') { - super.sendKeyChar((char)code); + sendDownUpKeyEventForBackwardCompatibility(code - '0' + KeyEvent.KEYCODE_0); if (ProductionFlag.IS_EXPERIMENTAL) { ResearchLogger.latinIME_sendKeyCodePoint(code); } @@ -1245,7 +1271,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // a hardware keyboard event on pressing enter or delete. This is bad for many // reasons (there are race conditions with commits) but some applications are // relying on this behavior so we continue to support it for older apps. - sendUpDownEnterOrBackspace(KeyEvent.KEYCODE_ENTER); + sendDownUpKeyEventForBackwardCompatibility(KeyEvent.KEYCODE_ENTER); } else { final String text = new String(new int[] { code }, 0, 1); mConnection.commitText(text, text.length()); @@ -1254,7 +1280,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // Implementation of {@link KeyboardActionListener}. @Override - public void onCodeInput(int primaryCode, int x, int y) { + public void onCodeInput(final int primaryCode, final int x, final int y) { final long when = SystemClock.uptimeMillis(); if (primaryCode != Keyboard.CODE_DELETE || when > mLastKeyTime + QUICK_PRESS) { mDeleteCount = 0; @@ -1309,7 +1335,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: @@ -1340,7 +1366,9 @@ 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); @@ -1349,10 +1377,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // Called from PointerTracker through the KeyboardActionListener interface @Override - public void onTextInput(CharSequence rawText) { + public void onTextInput(final CharSequence rawText) { mConnection.beginBatchEdit(); if (mWordComposer.isComposingWord()) { - commitCurrentAutoCorrection(LastComposedWord.NOT_A_SEPARATOR); + commitCurrentAutoCorrection(rawText.toString()); + } else { + resetComposingState(true /* alsoResetLastComposedWord */); } mHandler.postUpdateSuggestionStrip(); final CharSequence text = specificTldProcessingOnTextInput(rawText); @@ -1365,14 +1395,13 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mKeyboardSwitcher.onCodeInput(Keyboard.CODE_OUTPUT_TEXT); mSpaceState = SPACE_STATE_NONE; mEnteredText = text; - resetComposingState(true /* alsoResetLastComposedWord */); } @Override public void onStartBatchInput() { mConnection.beginBatchEdit(); if (mWordComposer.isComposingWord()) { - commitCurrentAutoCorrection(LastComposedWord.NOT_A_SEPARATOR); + commitTyped(LastComposedWord.NOT_A_SEPARATOR); mExpectingUpdateSelection = true; // TODO: Can we remove this? mSpaceState = SPACE_STATE_PHANTOM; @@ -1382,40 +1411,102 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode()); } - @Override - public void onUpdateBatchInput(InputPointers batchPointers) { - mWordComposer.setBatchInputPointers(batchPointers); - final SuggestedWords suggestedWords = getSuggestedWords(); - showSuggestionStrip(suggestedWords, null); - final String gestureFloatingPreviewText = (suggestedWords.size() > 0) + private static final class BatchInputUpdater implements Handler.Callback { + private final Handler mHandler; + private LatinIME mLatinIme; + + private BatchInputUpdater() { + final HandlerThread handlerThread = new HandlerThread( + BatchInputUpdater.class.getSimpleName()); + handlerThread.start(); + mHandler = new Handler(handlerThread.getLooper(), this); + } + + // Initialization-on-demand holder + private static final class OnDemandInitializationHolder { + public static final BatchInputUpdater sInstance = new BatchInputUpdater(); + } + + public static BatchInputUpdater getInstance() { + return OnDemandInitializationHolder.sInstance; + } + + private static final int MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP = 1; + + @Override + public boolean handleMessage(final Message msg) { + switch (msg.what) { + case MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP: + final SuggestedWords suggestedWords = getSuggestedWordsGesture( + (InputPointers)msg.obj, mLatinIme); + showGesturePreviewAndSuggestionStrip( + suggestedWords, false /* dismissGestureFloatingPreviewText */, mLatinIme); + break; + } + return true; + } + + public void updateGesturePreviewAndSuggestionStrip(final InputPointers batchPointers, + final LatinIME latinIme) { + mLatinIme = latinIme; + if (mHandler.hasMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP)) { + return; + } + mHandler.obtainMessage( + MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, batchPointers) + .sendToTarget(); + } + + public void showGesturePreviewAndSuggestionStrip(final SuggestedWords suggestedWords, + final boolean dismissGestureFloatingPreviewText, final LatinIME latinIme) { + latinIme.mHandler.showGesturePreviewAndSuggestionStrip( + suggestedWords, dismissGestureFloatingPreviewText); + } + + // {@link LatinIME#getSuggestedWords(int)} method calls with same session id have to + // be synchronized. + public synchronized SuggestedWords getSuggestedWordsGesture( + final InputPointers batchPointers, final LatinIME latinIme) { + latinIme.mWordComposer.setBatchInputPointers(batchPointers); + return latinIme.getSuggestedWords(Suggest.SESSION_GESTURE); + } + } + + private void showGesturePreviewAndSuggestionStrip(final SuggestedWords suggestedWords, + final boolean dismissGestureFloatingPreviewText) { + final String batchInputText = (suggestedWords.size() > 0) ? suggestedWords.getWord(0) : null; - mKeyboardSwitcher.getMainKeyboardView() - .showGestureFloatingPreviewText(gestureFloatingPreviewText); + final KeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); + mainKeyboardView.showGestureFloatingPreviewText(batchInputText); + showSuggestionStrip(suggestedWords, null); + if (dismissGestureFloatingPreviewText) { + mainKeyboardView.dismissGestureFloatingPreviewText(); + } } @Override - public void onEndBatchInput(InputPointers batchPointers) { - mWordComposer.setBatchInputPointers(batchPointers); - final SuggestedWords suggestedWords = getSuggestedWords(); - showSuggestionStrip(suggestedWords, null); - final String gestureFloatingPreviewText = (suggestedWords.size() > 0) + public void onUpdateBatchInput(final InputPointers batchPointers) { + BatchInputUpdater.getInstance().updateGesturePreviewAndSuggestionStrip(batchPointers, this); + } + + @Override + public void onEndBatchInput(final InputPointers batchPointers) { + final BatchInputUpdater batchInputUpdater = BatchInputUpdater.getInstance(); + final SuggestedWords suggestedWords = batchInputUpdater.getSuggestedWordsGesture( + batchPointers, this); + batchInputUpdater.showGesturePreviewAndSuggestionStrip( + suggestedWords, true /* dismissGestureFloatingPreviewText */, this); + final String batchInputText = (suggestedWords.size() > 0) ? suggestedWords.getWord(0) : null; - final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); - mainKeyboardView.showGestureFloatingPreviewText(gestureFloatingPreviewText); - mainKeyboardView.dismissGestureFloatingPreviewText(); - if (suggestedWords == null || suggestedWords.size() == 0) { + if (TextUtils.isEmpty(batchInputText)) { return; } - final CharSequence text = suggestedWords.getWord(0); - if (TextUtils.isEmpty(text)) { - return; - } - mWordComposer.setBatchInputWord(text); + mWordComposer.setBatchInputWord(batchInputText); mConnection.beginBatchEdit(); if (SPACE_STATE_PHANTOM == mSpaceState) { sendKeyCodePoint(Keyboard.CODE_SPACE); } - mConnection.setComposingText(text, 1); + mConnection.setComposingText(batchInputText, 1); mExpectingUpdateSelection = true; mConnection.endBatchEdit(); mKeyboardSwitcher.updateShiftState(); @@ -1451,18 +1542,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 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) { @@ -1483,6 +1562,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()) { @@ -1518,7 +1609,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // a hardware keyboard event on pressing enter or delete. This is bad for many // reasons (there are race conditions with commits) but some applications are // relying on this behavior so we continue to support it for older apps. - sendUpDownEnterOrBackspace(KeyEvent.KEYCODE_DEL); + sendDownUpKeyEventForBackwardCompatibility(KeyEvent.KEYCODE_DEL); } else { mConnection.deleteSurroundingText(1, 0); } @@ -1626,10 +1717,11 @@ 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)); } } @@ -1660,7 +1752,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen swapSwapperAndSpace(); mSpaceState = SPACE_STATE_SWAP_PUNCTUATION; } else if (SPACE_STATE_PHANTOM == spaceState - && !mCurrentSettings.isWeakSpaceStripper(primaryCode)) { + && !mCurrentSettings.isWeakSpaceStripper(primaryCode) + && !mCurrentSettings.isPhantomSpacePromotingSymbol(primaryCode)) { // If we are in phantom space state, and the user presses a separator, we want to // stay in phantom space state so that the next keypress has a chance to add the // space. For example, if I type "Good dat", pick "day" from the suggestion strip @@ -1761,12 +1854,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen return; } - final SuggestedWords suggestedWords = getSuggestedWords(); + final SuggestedWords suggestedWords = getSuggestedWords(Suggest.SESSION_TYPING); final String typedWord = mWordComposer.getTypedWord(); showSuggestionStrip(suggestedWords, typedWord); } - private SuggestedWords getSuggestedWords() { + private SuggestedWords getSuggestedWords(final int sessionId) { final String typedWord = mWordComposer.getTypedWord(); // Get the word on which we should search the bigrams. If we are composing a word, it's // whatever is *before* the half-committed word in the buffer, hence 2; if we aren't, we @@ -1777,7 +1870,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mWordComposer.isComposingWord() ? 2 : 1); final SuggestedWords suggestedWords = mSuggest.getSuggestedWords(mWordComposer, prevWord, mKeyboardSwitcher.getKeyboard().getProximityInfo(), - mCurrentSettings.mCorrectionEnabled); + mCurrentSettings.mCorrectionEnabled, sessionId); return maybeRetrieveOlderSuggestions(typedWord, suggestedWords); } @@ -1834,7 +1927,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(); @@ -1848,13 +1941,17 @@ 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); + 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. + // to the user that auto-correction happened. It has no other effect; in particular + // note that this won't affect the text inside the text field AT ALL: it only makes + // the segment of text starting at the supplied index and running for the length + // of the auto-correction flash. At this moment, the "typedWord" argument is + // ignored by TextView. mConnection.commitCorrection( new CorrectionInfo(mLastSelectionEnd - typedWord.length(), typedWord, autoCorrection)); @@ -1949,7 +2046,7 @@ 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); @@ -1960,7 +2057,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() { @@ -2030,7 +2127,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) { @@ -2051,10 +2148,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mUserHistoryDictionary.cancelAddingUserHistory( previousWord.toString(), committedWord.toString()); } - mConnection.commitText(originallyTypedWord, 1); - // Re-insert the separator - sendKeyCodePoint(mLastComposedWord.mSeparatorCode); - Utils.Stats.onSeparator(mLastComposedWord.mSeparatorCode, + 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); @@ -2067,7 +2162,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } // Used by the RingCharBuffer - public boolean isWordSeparator(int code) { + public boolean isWordSeparator(final int code) { return mCurrentSettings.isWordSeparator(code); } @@ -2099,14 +2194,14 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // Callback called by PointerTracker through the KeyboardActionListener. This is called when a // key is depressed; release matching call is onReleaseKey below. @Override - public void onPressKey(int primaryCode) { + public void onPressKey(final int primaryCode) { mKeyboardSwitcher.onPressKey(primaryCode); } // Callback by PointerTracker through the KeyboardActionListener. This is called when a key // is released; press matching call is onPressKey above. @Override - public void onReleaseKey(int primaryCode, boolean withSliding) { + public void onReleaseKey(final int primaryCode, final boolean withSliding) { mKeyboardSwitcher.onReleaseKey(primaryCode, withSliding); // If accessibility is on, ensure the user receives keyboard state updates. @@ -2135,7 +2230,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // receive ringer mode change and network state change. private BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override - public void onReceive(Context context, Intent intent) { + public void onReceive(final Context context, final Intent intent) { final String action = intent.getAction(); if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) { mSubtypeSwitcher.onNetworkStateChanged(intent); @@ -2156,14 +2251,14 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen launchSubActivity(DebugSettingsActivity.class); } - public void launchKeyboardedDialogActivity(Class<? extends Activity> activityClass) { + public void launchKeyboardedDialogActivity(final Class<? extends Activity> activityClass) { // Put the text in the attached EditText into a safe, saved state before switching to a // new activity that will also use the soft keyboard. commitTyped(LastComposedWord.NOT_A_SEPARATOR); launchSubActivity(activityClass); } - private void launchSubActivity(Class<? extends Activity> activityClass) { + private void launchSubActivity(final Class<? extends Activity> activityClass) { Intent intent = new Intent(); intent.setClass(LatinIME.this, activityClass); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); @@ -2203,7 +2298,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen showOptionDialog(builder.create()); } - public void showOptionDialog(AlertDialog dialog) { + public void showOptionDialog(final AlertDialog dialog) { final IBinder windowToken = mKeyboardSwitcher.getMainKeyboardView().getWindowToken(); if (windowToken == null) { return; @@ -2223,8 +2318,19 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen dialog.show(); } + public void debugDumpStateAndCrashWithException(final String context) { + final StringBuilder s = new StringBuilder(); + s.append("Target application : ").append(mTargetApplicationInfo.name) + .append("\nPackage : ").append(mTargetApplicationInfo.packageName) + .append("\nTarget app sdk version : ") + .append(mTargetApplicationInfo.targetSdkVersion) + .append("\nAttributes : ").append(mCurrentSettings.getInputAttributesDebugString()) + .append("\nContext : ").append(context); + throw new RuntimeException(s.toString()); + } + @Override - protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) { + protected void dump(final FileDescriptor fd, final PrintWriter fout, final String[] args) { super.dump(fd, fout, args); final Printer p = new PrintWriterPrinter(fout); diff --git a/java/src/com/android/inputmethod/latin/LocaleUtils.java b/java/src/com/android/inputmethod/latin/LocaleUtils.java index 3b08cab01..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. } @@ -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/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 41e59e92d..b85f9dcd7 100644 --- a/java/src/com/android/inputmethod/latin/RichInputConnection.java +++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java @@ -30,19 +30,51 @@ import com.android.inputmethod.keyboard.Keyboard; import com.android.inputmethod.latin.define.ProductionFlag; import com.android.inputmethod.research.ResearchLogger; +import java.util.Locale; import java.util.regex.Pattern; /** - * Wrapper for InputConnection to simplify interaction + * Enrichment class for InputConnection to simplify interaction and add functionality. + * + * This class serves as a wrapper to be able to simply add hooks to any calls to the underlying + * InputConnection. It also keeps track of a number of things to avoid having to call upon IPC + * all the time to find out what text is in the buffer, when we need it to determine caps mode + * for example. */ public class RichInputConnection { private static final String TAG = RichInputConnection.class.getSimpleName(); private static final boolean DBG = false; + private static final boolean DEBUG_PREVIOUS_TEXT = false; // Provision for a long word pair and a separator private static final int LOOKBACK_CHARACTER_NUM = BinaryDictionary.MAX_WORD_LENGTH * 2 + 1; private static final Pattern spaceRegex = Pattern.compile("\\s+"); private static final int INVALID_CURSOR_POSITION = -1; + /** + * This variable contains the value LatinIME thinks the cursor position should be at now. + * This is a few steps in advance of what the TextView thinks it is, because TextView will + * only know after the IPC calls gets through. + */ + private int mCurrentCursorPosition = INVALID_CURSOR_POSITION; // in chars, not code points + /** + * This contains the committed text immediately preceding the cursor and the composing + * text if any. It is refreshed when the cursor moves by calling upon the TextView. + */ + private StringBuilder mCommittedTextBeforeComposingText = new StringBuilder(); + /** + * This contains the currently composing text, as LatinIME thinks the TextView is seeing it. + */ + private StringBuilder mComposingText = new StringBuilder(); + /** + * This is a one-character string containing the character after the cursor. Since LatinIME + * never touches it directly, it's never modified by any means other than re-reading from the + * TextView when the cursor position is changed by the user. + */ + private CharSequence mCharAfterTheCursor = ""; + // A hint on how many characters to cache from the TextView. A good value of this is given by + // how many characters we need to be able to almost always find the caps mode. + private static final int DEFAULT_TEXT_CACHE_SIZE = 100; + private final InputMethodService mParent; InputConnection mIC; int mNestLevel; @@ -52,6 +84,37 @@ public class RichInputConnection { mNestLevel = 0; } + private void checkConsistencyForDebug() { + final ExtractedTextRequest r = new ExtractedTextRequest(); + r.hintMaxChars = 0; + r.hintMaxLines = 0; + r.token = 1; + r.flags = 0; + final ExtractedText et = mIC.getExtractedText(r, 0); + final CharSequence beforeCursor = getTextBeforeCursor(DEFAULT_TEXT_CACHE_SIZE, 0); + final StringBuilder internal = new StringBuilder().append(mCommittedTextBeforeComposingText) + .append(mComposingText); + if (null == et || null == beforeCursor) return; + final int actualLength = Math.min(beforeCursor.length(), internal.length()); + if (internal.length() > actualLength) { + internal.delete(0, internal.length() - actualLength); + } + final String reference = (beforeCursor.length() <= actualLength) ? beforeCursor.toString() + : beforeCursor.subSequence(beforeCursor.length() - actualLength, + beforeCursor.length()).toString(); + if (et.selectionStart != mCurrentCursorPosition + || !(reference.equals(internal.toString()))) { + final String context = "Expected cursor position = " + mCurrentCursorPosition + + "\nActual cursor position = " + et.selectionStart + + "\nExpected text = " + internal.length() + " " + internal + + "\nActual text = " + reference.length() + " " + reference; + ((LatinIME)mParent).debugDumpStateAndCrashWithException(context); + } else { + Log.e(TAG, Utils.getStackTrace(2)); + Log.e(TAG, "Exp <> Actual : " + mCurrentCursorPosition + " <> " + et.selectionStart); + } + } + public void beginBatchEdit() { if (++mNestLevel == 1) { mIC = mParent.getCurrentInputConnection(); @@ -65,12 +128,30 @@ public class RichInputConnection { Log.e(TAG, "Nest level too deep : " + mNestLevel); } } + checkBatchEdit(); + if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); } + 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 (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); + } + + public void resetCachesUponCursorMove(final int newCursorPosition) { + mCurrentCursorPosition = newCursorPosition; + mComposingText.setLength(0); + mCommittedTextBeforeComposingText.setLength(0); + mCommittedTextBeforeComposingText.append(getTextBeforeCursor(DEFAULT_TEXT_CACHE_SIZE, 0)); + mCharAfterTheCursor = getTextAfterCursor(1, 0); + if (null != mIC) { + mIC.finishComposingText(); + if (ProductionFlag.IS_EXPERIMENTAL) { + ResearchLogger.richInputConnection_finishComposingText(); + } + } } private void checkBatchEdit() { @@ -83,6 +164,10 @@ public class RichInputConnection { public void finishComposingText() { checkBatchEdit(); + if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); + mCommittedTextBeforeComposingText.append(mComposingText); + mCurrentCursorPosition += mComposingText.length(); + mComposingText.setLength(0); if (null != mIC) { mIC.finishComposingText(); if (ProductionFlag.IS_EXPERIMENTAL) { @@ -93,6 +178,10 @@ public class RichInputConnection { public void commitText(final CharSequence text, final int i) { checkBatchEdit(); + if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); + mCommittedTextBeforeComposingText.append(text); + mCurrentCursorPosition += text.length() - mComposingText.length(); + mComposingText.setLength(0); if (null != mIC) { mIC.commitText(text, i); if (ProductionFlag.IS_EXPERIMENTAL) { @@ -101,10 +190,22 @@ public class RichInputConnection { } } - public int getCursorCapsMode(final int inputType) { + public int getCursorCapsMode(final int inputType, final Locale locale) { mIC = mParent.getCurrentInputConnection(); if (null == mIC) return Constants.TextUtils.CAP_MODE_OFF; - return mIC.getCursorCapsMode(inputType); + if (!TextUtils.isEmpty(mComposingText)) return Constants.TextUtils.CAP_MODE_OFF; + // TODO: this will generally work, but there may be cases where the buffer contains SOME + // information but not enough to determine the caps mode accurately. This may happen after + // heavy pressing of delete, for example DEFAULT_TEXT_CACHE_SIZE - 5 times or so. + // getCapsMode should be updated to be able to return a "not enough info" result so that + // we can get more context only when needed. + if (TextUtils.isEmpty(mCommittedTextBeforeComposingText) && 0 != mCurrentCursorPosition) { + mCommittedTextBeforeComposingText.append( + getTextBeforeCursor(DEFAULT_TEXT_CACHE_SIZE, 0)); + } + // This never calls InputConnection#getCapsMode - in fact, it's a static method that + // never blocks or initiates IPC. + return StringUtils.getCapsMode(mCommittedTextBeforeComposingText, inputType, locale); } public CharSequence getTextBeforeCursor(final int i, final int j) { @@ -121,12 +222,28 @@ public class RichInputConnection { public void deleteSurroundingText(final int i, final int j) { checkBatchEdit(); + final int remainingChars = mComposingText.length() - i; + if (remainingChars >= 0) { + mComposingText.setLength(remainingChars); + } else { + mComposingText.setLength(0); + // Never cut under 0 + final int len = Math.max(mCommittedTextBeforeComposingText.length() + + remainingChars, 0); + mCommittedTextBeforeComposingText.setLength(len); + } + if (mCurrentCursorPosition > i) { + mCurrentCursorPosition -= i; + } else { + mCurrentCursorPosition = 0; + } if (null != mIC) { mIC.deleteSurroundingText(i, j); if (ProductionFlag.IS_EXPERIMENTAL) { ResearchLogger.richInputConnection_deleteSurroundingText(i, j); } } + if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); } public void performEditorAction(final int actionId) { @@ -141,6 +258,44 @@ public class RichInputConnection { public void sendKeyEvent(final KeyEvent keyEvent) { checkBatchEdit(); + if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) { + if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); + // This method is only called for enter or backspace when speaking to old + // applications (target SDK <= 15), or for digits. + // When talking to new applications we never use this method because it's inherently + // racy and has unpredictable results, but for backward compatibility we continue + // sending the key events for only Enter and Backspace because some applications + // mistakenly catch them to do some stuff. + switch (keyEvent.getKeyCode()) { + case KeyEvent.KEYCODE_ENTER: + mCommittedTextBeforeComposingText.append("\n"); + mCurrentCursorPosition += 1; + break; + case KeyEvent.KEYCODE_DEL: + if (0 == mComposingText.length()) { + if (mCommittedTextBeforeComposingText.length() > 0) { + mCommittedTextBeforeComposingText.delete( + mCommittedTextBeforeComposingText.length() - 1, + mCommittedTextBeforeComposingText.length()); + } + } else { + mComposingText.delete(mComposingText.length() - 1, mComposingText.length()); + } + if (mCurrentCursorPosition > 0) mCurrentCursorPosition -= 1; + break; + case KeyEvent.KEYCODE_UNKNOWN: + if (null != keyEvent.getCharacters()) { + mCommittedTextBeforeComposingText.append(keyEvent.getCharacters()); + mCurrentCursorPosition += keyEvent.getCharacters().length(); + } + break; + default: + final String text = new String(new int[] { keyEvent.getUnicodeChar() }, 0, 1); + mCommittedTextBeforeComposingText.append(text); + mCurrentCursorPosition += text.length(); + break; + } + } if (null != mIC) { mIC.sendKeyEvent(keyEvent); if (ProductionFlag.IS_EXPERIMENTAL) { @@ -151,48 +306,83 @@ public class RichInputConnection { public void setComposingText(final CharSequence text, final int i) { checkBatchEdit(); + if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); + mCurrentCursorPosition += text.length() - mComposingText.length(); + mComposingText.setLength(0); + mComposingText.append(text); + // TODO: support values of i != 1. At this time, this is never called with i != 1. if (null != mIC) { mIC.setComposingText(text, i); if (ProductionFlag.IS_EXPERIMENTAL) { ResearchLogger.richInputConnection_setComposingText(text, i); } } + if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); } public void setSelection(final int from, final int to) { checkBatchEdit(); + if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); if (null != mIC) { mIC.setSelection(from, to); if (ProductionFlag.IS_EXPERIMENTAL) { ResearchLogger.richInputConnection_setSelection(from, to); } } + mCurrentCursorPosition = from; + mCommittedTextBeforeComposingText.setLength(0); + mCommittedTextBeforeComposingText.append(getTextBeforeCursor(DEFAULT_TEXT_CACHE_SIZE, 0)); } public void commitCorrection(final CorrectionInfo correctionInfo) { checkBatchEdit(); + if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); + // This has no effect on the text field and does not change its content. It only makes + // TextView flash the text for a second based on indices contained in the argument. if (null != mIC) { mIC.commitCorrection(correctionInfo); if (ProductionFlag.IS_EXPERIMENTAL) { ResearchLogger.richInputConnection_commitCorrection(correctionInfo); } } + if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); } public void commitCompletion(final CompletionInfo completionInfo) { checkBatchEdit(); + if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); + final CharSequence text = completionInfo.getText(); + mCommittedTextBeforeComposingText.append(text); + mCurrentCursorPosition += text.length() - mComposingText.length(); + mComposingText.setLength(0); if (null != mIC) { mIC.commitCompletion(completionInfo); if (ProductionFlag.IS_EXPERIMENTAL) { ResearchLogger.richInputConnection_commitCompletion(completionInfo); } } + if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); } public CharSequence getNthPreviousWord(final String sentenceSeperators, final int n) { mIC = mParent.getCurrentInputConnection(); if (null == mIC) return null; final CharSequence prev = mIC.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0); + if (DEBUG_PREVIOUS_TEXT && null != prev) { + final int checkLength = LOOKBACK_CHARACTER_NUM - 1; + final String reference = prev.length() <= checkLength ? prev.toString() + : prev.subSequence(prev.length() - checkLength, prev.length()).toString(); + final StringBuilder internal = new StringBuilder() + .append(mCommittedTextBeforeComposingText).append(mComposingText); + if (internal.length() > checkLength) { + internal.delete(0, internal.length() - checkLength); + if (!(reference.equals(internal.toString()))) { + final String context = + "Expected text = " + internal + "\nActual text = " + reference; + ((LatinIME)mParent).debugDumpStateAndCrashWithException(context); + } + } + } return getNthPreviousWord(prev, sentenceSeperators, n); } @@ -452,4 +642,34 @@ public class RichInputConnection { commitText(" " + textBeforeCursor.subSequence(0, 1), 1); return true; } + + /** + * Heuristic to determine if this is an expected update of the cursor. + * + * Sometimes updates to the cursor position are late because of their asynchronous nature. + * This method tries to determine if this update is one, based on the values of the cursor + * position in the update, and the currently expected position of the cursor according to + * LatinIME's internal accounting. If this is not a belated expected update, then it should + * mean that the user moved the cursor explicitly. + * This is quite robust, but of course it's not perfect. In particular, it will fail in the + * case we get an update A, the user types in N characters so as to move the cursor to A+N but + * we don't get those, and then the user places the cursor between A and A+N, and we get only + * this update and not the ones in-between. This is almost impossible to achieve even trying + * very very hard. + * + * @param oldSelStart The value of the old cursor position in the update. + * @param newSelStart The value of the new cursor position in the update. + * @return whether this is a belated expected update or not. + */ + public boolean isBelatedExpectedUpdate(final int oldSelStart, final int newSelStart) { + // If this is an update that arrives at our expected position, it's a belated update. + if (newSelStart == mCurrentCursorPosition) return true; + // If this is an update that moves the cursor from our expected position, it must be + // an explicit move. + if (oldSelStart == mCurrentCursorPosition) return false; + // The following returns true if newSelStart is between oldSelStart and + // mCurrentCursorPosition. We assume that if the updated position is between the old + // position and the expected position, then it must be a belated update. + return (newSelStart - oldSelStart) * (mCurrentCursorPosition - newSelStart) >= 0; + } } diff --git a/java/src/com/android/inputmethod/latin/SettingsValues.java b/java/src/com/android/inputmethod/latin/SettingsValues.java index dcd2532c1..5e9c870d4 100644 --- a/java/src/com/android/inputmethod/latin/SettingsValues.java +++ b/java/src/com/android/inputmethod/latin/SettingsValues.java @@ -35,7 +35,7 @@ import java.util.HashMap; * When you call the constructor of this class, you may want to change the current system locale by * using {@link LocaleUtils.RunInLocale}. */ -public class SettingsValues { +public final class SettingsValues { private static final String TAG = SettingsValues.class.getSimpleName(); private static final int SUGGESTION_VISIBILITY_SHOW_VALUE @@ -246,64 +246,65 @@ public class SettingsValues { && orientation == Configuration.ORIENTATION_PORTRAIT); } - public boolean isWordSeparator(int code) { + public boolean isWordSeparator(final int code) { return mWordSeparators.contains(String.valueOf((char)code)); } - public boolean isSymbolExcludedFromWordSeparators(int code) { + public boolean isSymbolExcludedFromWordSeparators(final int code) { return mSymbolsExcludedFromWordSeparators.contains(String.valueOf((char)code)); } - public boolean isWeakSpaceStripper(int code) { + public boolean isWeakSpaceStripper(final int code) { // TODO: this does not work if the code does not fit in a char return mWeakSpaceStrippers.contains(String.valueOf((char)code)); } - public boolean isWeakSpaceSwapper(int code) { + public boolean isWeakSpaceSwapper(final int code) { // TODO: this does not work if the code does not fit in a char return mWeakSpaceSwappers.contains(String.valueOf((char)code)); } - public boolean isPhantomSpacePromotingSymbol(int code) { + public boolean isPhantomSpacePromotingSymbol(final int code) { // TODO: this does not work if the code does not fit in a char return mPhantomSpacePromotingSymbols.contains(String.valueOf((char)code)); } - private static boolean isAutoCorrectEnabled(final Resources resources, + private static boolean isAutoCorrectEnabled(final Resources res, final String currentAutoCorrectionSetting) { - final String autoCorrectionOff = resources.getString( + final String autoCorrectionOff = res.getString( R.string.auto_correction_threshold_mode_index_off); return !currentAutoCorrectionSetting.equals(autoCorrectionOff); } // Public to access from KeyboardSwitcher. Should it have access to some // process-global instance instead? - public static boolean isKeyPreviewPopupEnabled(SharedPreferences sp, Resources resources) { - final boolean showPopupOption = resources.getBoolean( + public static boolean isKeyPreviewPopupEnabled(final SharedPreferences prefs, + final Resources res) { + final boolean showPopupOption = res.getBoolean( R.bool.config_enable_show_popup_on_keypress_option); - if (!showPopupOption) return resources.getBoolean(R.bool.config_default_popup_preview); - return sp.getBoolean(Settings.PREF_POPUP_ON, - resources.getBoolean(R.bool.config_default_popup_preview)); + if (!showPopupOption) return res.getBoolean(R.bool.config_default_popup_preview); + return prefs.getBoolean(Settings.PREF_POPUP_ON, + res.getBoolean(R.bool.config_default_popup_preview)); } // Likewise - public static int getKeyPreviewPopupDismissDelay(SharedPreferences sp, - Resources resources) { + public static int getKeyPreviewPopupDismissDelay(final SharedPreferences prefs, + final Resources res) { // TODO: use mKeyPreviewPopupDismissDelayRawValue instead of reading it again here. - return Integer.parseInt(sp.getString(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY, - Integer.toString(resources.getInteger( + return Integer.parseInt(prefs.getString(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY, + Integer.toString(res.getInteger( R.integer.config_key_preview_linger_timeout)))); } - private static boolean isBigramPredictionEnabled(final SharedPreferences sp, - final Resources resources) { - return sp.getBoolean(Settings.PREF_BIGRAM_PREDICTIONS, resources.getBoolean( + private static boolean isBigramPredictionEnabled(final SharedPreferences prefs, + final Resources res) { + return prefs.getBoolean(Settings.PREF_BIGRAM_PREDICTIONS, res.getBoolean( R.bool.config_default_next_word_prediction)); } - private static float getAutoCorrectionThreshold(final Resources resources, + private static float getAutoCorrectionThreshold(final Resources res, final String currentAutoCorrectionSetting) { - final String[] autoCorrectionThresholdValues = resources.getStringArray( + final String[] autoCorrectionThresholdValues = res.getStringArray( R.array.auto_correction_threshold_values); // When autoCorrectionThreshold is greater than 1.0, it's like auto correction is off. float autoCorrectionThreshold = Float.MAX_VALUE; @@ -335,11 +336,11 @@ public class SettingsValues { return mVoiceKeyOnMain; } - public static boolean isLanguageSwitchKeySupressed(SharedPreferences sp) { - return sp.getBoolean(Settings.PREF_SUPPRESS_LANGUAGE_SWITCH_KEY, false); + public static boolean isLanguageSwitchKeySupressed(final SharedPreferences prefs) { + return prefs.getBoolean(Settings.PREF_SUPPRESS_LANGUAGE_SWITCH_KEY, false); } - public boolean isLanguageSwitchKeyEnabled(Context context) { + public boolean isLanguageSwitchKeyEnabled(final Context context) { if (mIsLanguageSwitchKeySuppressed) { return false; } @@ -352,7 +353,7 @@ public class SettingsValues { } } - public boolean isFullscreenModeAllowed(Resources res) { + public boolean isFullscreenModeAllowed(final Resources res) { return res.getBoolean(R.bool.config_use_fullscreen_mode); } @@ -362,34 +363,35 @@ public class SettingsValues { public static String getPrefAdditionalSubtypes(final SharedPreferences prefs, final Resources res) { - final String prefSubtypes = res.getString(R.string.predefined_subtypes, ""); - return prefs.getString(Settings.PREF_CUSTOM_INPUT_STYLES, prefSubtypes); + final String predefinedPrefSubtypes = AdditionalSubtype.createPrefSubtypes( + res.getStringArray(R.array.predefined_subtypes)); + return prefs.getString(Settings.PREF_CUSTOM_INPUT_STYLES, predefinedPrefSubtypes); } // Accessed from the settings interface, hence public - public static float getCurrentKeypressSoundVolume(final SharedPreferences sp, - final Resources res) { + public static float getCurrentKeypressSoundVolume(final SharedPreferences prefs, + final Resources res) { // TODO: use mVibrationDurationSettingsRawValue instead of reading it again here - final float volume = sp.getFloat(Settings.PREF_KEYPRESS_SOUND_VOLUME, -1.0f); + final float volume = prefs.getFloat(Settings.PREF_KEYPRESS_SOUND_VOLUME, -1.0f); if (volume >= 0) { 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 - public static int getCurrentVibrationDuration(final SharedPreferences sp, - final Resources res) { + public static int getCurrentVibrationDuration(final SharedPreferences prefs, + final Resources res) { // TODO: use mKeypressVibrationDuration instead of reading it again here - final int ms = sp.getInt(Settings.PREF_VIBRATION_DURATION_SETTINGS, -1); + final int ms = prefs.getInt(Settings.PREF_VIBRATION_DURATION_SETTINGS, -1); if (ms >= 0) { 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 @@ -398,22 +400,22 @@ public class SettingsValues { return prefs.getBoolean(Settings.PREF_USABILITY_STUDY_MODE, true); } - public static long getLastUserHistoryWriteTime( - final SharedPreferences prefs, final String locale) { + 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); } return 0; } - public static void setLastUserHistoryWriteTime( - final SharedPreferences prefs, final String locale) { + 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(); } diff --git a/java/src/com/android/inputmethod/latin/StringUtils.java b/java/src/com/android/inputmethod/latin/StringUtils.java index 39c59b44c..6dc1ea807 100644 --- a/java/src/com/android/inputmethod/latin/StringUtils.java +++ b/java/src/com/android/inputmethod/latin/StringUtils.java @@ -18,10 +18,12 @@ package com.android.inputmethod.latin; import android.text.TextUtils; +import com.android.inputmethod.keyboard.Keyboard; // For character constants + import java.util.ArrayList; import java.util.Locale; -public class StringUtils { +public final class StringUtils { private StringUtils() { // This utility class is not publicly instantiable. } @@ -123,23 +125,6 @@ public class StringUtils { } /** - * Returns true if cs contains any upper case characters. - * - * @param cs the CharSequence to check - * @return {@code true} if cs contains any upper case characters, {@code false} otherwise. - */ - public static boolean hasUpperCase(final CharSequence cs) { - final int length = cs.length(); - for (int i = 0, cp = 0; i < length; i += Character.charCount(cp)) { - cp = Character.codePointAt(cs, i); - if (Character.isUpperCase(cp)) { - return true; - } - } - return false; - } - - /** * Remove duplicates from an array of strings. * * This method will always keep the first occurrence of all strings at their position @@ -197,4 +182,200 @@ public class StringUtils { codePoints[dsti] = codePoint; return codePoints; } + + /** + * Determine what caps mode should be in effect at the current offset in + * the text. Only the mode bits set in <var>reqModes</var> will be + * checked. Note that the caps mode flags here are explicitly defined + * to match those in {@link InputType}. + * + * This code is a straight copy of TextUtils.getCapsMode (modulo namespace and formatting + * issues). This will change in the future as we simplify the code for our use and fix bugs. + * + * @param cs The text that should be checked for caps modes. + * @param reqModes The modes to be checked: may be any combination of + * {@link TextUtils#CAP_MODE_CHARACTERS}, {@link TextUtils#CAP_MODE_WORDS}, and + * {@link TextUtils#CAP_MODE_SENTENCES}. + * @param locale The locale to consider for capitalization rules + * + * @return Returns the actual capitalization modes that can be in effect + * at the current position, which is any combination of + * {@link TextUtils#CAP_MODE_CHARACTERS}, {@link TextUtils#CAP_MODE_WORDS}, and + * {@link TextUtils#CAP_MODE_SENTENCES}. + */ + public static int getCapsMode(final CharSequence cs, final int reqModes, final Locale locale) { + // Quick description of what we want to do: + // CAP_MODE_CHARACTERS is always on. + // CAP_MODE_WORDS is on if there is some whitespace before the cursor. + // CAP_MODE_SENTENCES is on if there is some whitespace before the cursor, and the end + // of a sentence just before that. + // We ignore opening parentheses and the like just before the cursor for purposes of + // finding whitespace for WORDS and SENTENCES modes. + // The end of a sentence ends with a period, question mark or exclamation mark. If it's + // a period, it also needs not to be an abbreviation, which means it also needs to either + // be immediately preceded by punctuation, or by a string of only letters with single + // periods interleaved. + + // Step 1 : check for cap MODE_CHARACTERS. If it's looked for, it's always on. + if ((reqModes & (TextUtils.CAP_MODE_WORDS | TextUtils.CAP_MODE_SENTENCES)) == 0) { + // Here we are not looking for MODE_WORDS or MODE_SENTENCES, so since we already + // evaluated MODE_CHARACTERS, we can return. + return TextUtils.CAP_MODE_CHARACTERS & reqModes; + } + + // Step 2 : Skip (ignore at the end of input) any opening punctuation. This includes + // opening parentheses, brackets, opening quotes, everything that *opens* a span of + // text in the linguistic sense. In RTL languages, this is still an opening sign, although + // it may look like a right parenthesis for example. We also include double quote and + // single quote since they aren't start punctuation in the unicode sense, but should still + // be skipped for English. TODO: does this depend on the language? + int i; + for (i = cs.length(); i > 0; i--) { + final char c = cs.charAt(i - 1); + if (c != Keyboard.CODE_DOUBLE_QUOTE && c != Keyboard.CODE_SINGLE_QUOTE + && Character.getType(c) != Character.START_PUNCTUATION) { + break; + } + } + + // We are now on the character that precedes any starting punctuation, so in the most + // frequent case this will be whitespace or a letter, although it may occasionally be a + // start of line, or some symbol. + + // Step 3 : Search for the start of a paragraph. From the starting point computed in step 2, + // we go back over any space or tab char sitting there. We find the start of a paragraph + // if the first char that's not a space or tab is a start of line (as in, either \n or + // start of text). + int j = i; + while (j > 0 && Character.isWhitespace(cs.charAt(j - 1))) { + j--; + } + if (j == 0) { + // There is only whitespace between the start of the text and the cursor. Both + // MODE_WORDS and MODE_SENTENCES should be active. + return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS + | TextUtils.CAP_MODE_SENTENCES) & reqModes; + } + if (i == j) { + // If we don't have whitespace before index i, it means neither MODE_WORDS + // nor mode sentences should be on so we can return right away. + return TextUtils.CAP_MODE_CHARACTERS & reqModes; + } + if ((reqModes & TextUtils.CAP_MODE_SENTENCES) == 0) { + // Here we know we have whitespace before the cursor (if not, we returned in the above + // if i == j clause), so we need MODE_WORDS to be on. And we don't need to evaluate + // MODE_SENTENCES so we can return right away. + return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & reqModes; + } + // Please note that because of the reqModes & CAP_MODE_SENTENCES test a few lines above, + // we know that MODE_SENTENCES is being requested. + + // Step 4 : Search for MODE_SENTENCES. + // English is a special case in that "American typography" rules, which are the most common + // in English, state that a sentence terminator immediately following a quotation mark + // should be swapped with it and de-duplicated (included in the quotation mark), + // e.g. <<Did he say, "let's go home?">> + // No other language has such a rule as far as I know, instead putting inside the quotation + // mark as the exact thing quoted and handling the surrounding punctuation independently, + // e.g. <<Did he say, "let's go home"?>> + // Hence, specifically for English, we treat this special case here. + if (Locale.ENGLISH.getLanguage().equals(locale.getLanguage())) { + for (; j > 0; j--) { + // Here we look to go over any closing punctuation. This is because in dominant + // variants of English, the final period is placed within double quotes and maybe + // other closing punctuation signs. This is generally not true in other languages. + final char c = cs.charAt(j - 1); + if (c != Keyboard.CODE_DOUBLE_QUOTE && c != Keyboard.CODE_SINGLE_QUOTE + && Character.getType(c) != Character.END_PUNCTUATION) { + break; + } + } + } + + if (j <= 0) return TextUtils.CAP_MODE_CHARACTERS & reqModes; + char c = cs.charAt(--j); + + // We found the next interesting chunk of text ; next we need to determine if it's the + // end of a sentence. If we have a question mark or an exclamation mark, it's the end of + // a sentence. If it's neither, the only remaining case is the period so we get the opposite + // case out of the way. + if (c == Keyboard.CODE_QUESTION_MARK || c == Keyboard.CODE_EXCLAMATION_MARK) { + return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_SENTENCES) & reqModes; + } + if (c != Keyboard.CODE_PERIOD || j <= 0) { + return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & reqModes; + } + + // We found out that we have a period. We need to determine if this is a full stop or + // otherwise sentence-ending period, or an abbreviation like "e.g.". An abbreviation + // looks like (\w\.){2,} + // To find out, we will have a simple state machine with the following states : + // START, WORD, PERIOD, ABBREVIATION + // On START : (just before the first period) + // letter => WORD + // whitespace => end with no caps (it was a stand-alone period) + // otherwise => end with caps (several periods/symbols in a row) + // On WORD : (within the word just before the first period) + // letter => WORD + // period => PERIOD + // otherwise => end with caps (it was a word with a full stop at the end) + // On PERIOD : (period within a potential abbreviation) + // letter => LETTER + // otherwise => end with caps (it was not an abbreviation) + // On LETTER : (letter within a potential abbreviation) + // letter => LETTER + // period => PERIOD + // otherwise => end with no caps (it was an abbreviation) + // "Not an abbreviation" in the above chart essentially covers cases like "...yes.". This + // should capitalize. + + final int START = 0; + final int WORD = 1; + final int PERIOD = 2; + final int LETTER = 3; + final int caps = (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS + | TextUtils.CAP_MODE_SENTENCES) & reqModes; + final int noCaps = (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & reqModes; + int state = START; + while (j > 0) { + c = cs.charAt(--j); + switch (state) { + case START: + if (Character.isLetter(c)) { + state = WORD; + } else if (Character.isWhitespace(c)) { + return noCaps; + } else { + return caps; + } + break; + case WORD: + if (Character.isLetter(c)) { + state = WORD; + } else if (c == Keyboard.CODE_PERIOD) { + state = PERIOD; + } else { + return caps; + } + break; + case PERIOD: + if (Character.isLetter(c)) { + state = LETTER; + } else { + return caps; + } + break; + case LETTER: + if (Character.isLetter(c)) { + state = LETTER; + } else if (c == Keyboard.CODE_PERIOD) { + state = PERIOD; + } else { + return noCaps; + } + } + } + // Here we arrived at the start of the line. This should behave exactly like whitespace. + return (START == state || LETTER == state) ? noCaps : caps; + } } diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java index 51ed09604..0418d3166 100644 --- a/java/src/com/android/inputmethod/latin/Suggest.java +++ b/java/src/com/android/inputmethod/latin/Suggest.java @@ -37,6 +37,11 @@ import java.util.concurrent.ConcurrentHashMap; public class Suggest { public static final String TAG = Suggest.class.getSimpleName(); + // Session id for + // {@link #getSuggestedWords(WordComposer,CharSequence,ProximityInfo,boolean,int)}. + public static final int SESSION_TYPING = 0; + public static final int SESSION_GESTURE = 1; + // TODO: rename this to CORRECTION_OFF public static final int CORRECTION_NONE = 0; // TODO: rename this to CORRECTION_ON @@ -157,13 +162,6 @@ 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()) { @@ -214,10 +212,12 @@ public class Suggest { whitelistedWord = suggestionsSet.first().mWord; } + // The word can be auto-corrected if it has a whitelist entry that is not itself, + // or if it's a 2+ characters non-word (i.e. it's not in the dictionary). final boolean allowsToBeAutoCorrected = (null != whitelistedWord && !whitelistedWord.equals(consideredWord)) - || AutoCorrection.isNotAWord(mDictionaries, consideredWord, - wordComposer.isFirstCharCapitalized()); + || (consideredWord.length() > 1 && !AutoCorrection.isInTheDictionary(mDictionaries, + consideredWord, wordComposer.isFirstCharCapitalized())); final boolean hasAutoCorrection; // TODO: using isCorrectionEnabled here is not very good. It's probably useless, because diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java index 68ecfa0d7..d9f48c4a4 100644 --- a/java/src/com/android/inputmethod/latin/SuggestedWords.java +++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java @@ -177,7 +177,7 @@ public class SuggestedWords { return; } int i = 1; - while(i < candidates.size()) { + while (i < candidates.size()) { final SuggestedWordInfo cur = candidates.get(i); for (int j = 0; j < i; ++j) { final SuggestedWordInfo previous = candidates.get(j); 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..550e4e58b --- /dev/null +++ b/java/src/com/android/inputmethod/latin/UserHistoryDictIOUtils.java @@ -0,0 +1,199 @@ +/* + * 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.FormatSpec.FormatOptions; +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; + + public 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; + } + + @Override + public void put(final byte b) { + mBuffer[mPosition++] = b; + } + } + + /** + * Writes dictionary to file. + */ + public static void writeDictionaryBinary(final OutputStream destination, + final BigramDictionaryInterface dict, final UserHistoryDictionaryBigramList bigrams, + final FormatOptions formatOptions) { + + final FusionDictionary fusionDict = constructFusionDictionary(dict, bigrams); + + try { + BinaryDictInputOutput.writeDictionaryBinary(destination, fusionDict, formatOptions); + } 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 6c9d1c250..683ee4f5c 100644 --- a/java/src/com/android/inputmethod/latin/UserHistoryDictionary.java +++ b/java/src/com/android/inputmethod/latin/UserHistoryDictionary.java @@ -182,6 +182,10 @@ public class UserHistoryDictionary extends ExpandableDictionary { * The second word may not be null (a NullPointerException would be thrown). */ public int addToUserHistory(final String word1, String word2, boolean isValid) { + if (word2.length() >= BinaryDictionary.MAX_WORD_LENGTH || + (word1 != null && word1.length() >= BinaryDictionary.MAX_WORD_LENGTH)) { + return -1; + } if (mBigramListLock.tryLock()) { try { super.addWord( 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 fc7a42100..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. } @@ -184,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"; @@ -393,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() { @@ -436,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 = - 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); - } - - private static final HashMap<String, Long> EMPTY_LT_HASH_MAP = CollectionUtils.newHashMap(); - 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 = 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/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/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java index ecec60f89..4b7adf26b 100644 --- a/java/src/com/android/inputmethod/latin/WordComposer.java +++ b/java/src/com/android/inputmethod/latin/WordComposer.java @@ -336,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 161b94ca0..6f508695e 100644 --- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java +++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java @@ -16,6 +16,8 @@ package com.android.inputmethod.latin.makedict; +import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader; +import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions; import com.android.inputmethod.latin.makedict.FusionDictionary.CharGroup; import com.android.inputmethod.latin.makedict.FusionDictionary.DictionaryOptions; import com.android.inputmethod.latin.makedict.FusionDictionary.Node; @@ -34,6 +36,7 @@ import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; import java.util.Map; +import java.util.Stack; import java.util.TreeMap; /** @@ -43,143 +46,7 @@ import java.util.TreeMap; */ public class BinaryDictInputOutput { - final static boolean DBG = MakedictLog.DBG; - - /* Node layout is as follows: - * | addressType xx : mask with MASK_GROUP_ADDRESS_TYPE - * 2 bits, 00 = no children : FLAG_GROUP_ADDRESS_TYPE_NOADDRESS - * f | 01 = 1 byte : FLAG_GROUP_ADDRESS_TYPE_ONEBYTE - * l | 10 = 2 bytes : FLAG_GROUP_ADDRESS_TYPE_TWOBYTES - * a | 11 = 3 bytes : FLAG_GROUP_ADDRESS_TYPE_THREEBYTES - * g | has several chars ? 1 bit, 1 = yes, 0 = no : FLAG_HAS_MULTIPLE_CHARS - * 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 - * - * c | IF FLAG_HAS_MULTIPLE_CHARS - * h | char, char, char, char n * (1 or 3 bytes) : use CharGroupInfo for i/o helpers - * a | end 1 byte, = 0 - * r | ELSE - * s | char 1 or 3 bytes - * | END - * - * f | - * r | IF FLAG_IS_TERMINAL - * e | frequency 1 byte - * q | - * - * c | IF 00 = FLAG_GROUP_ADDRESS_TYPE_NOADDRESS = addressType - * h | // nothing - * i | ELSIF 01 = FLAG_GROUP_ADDRESS_TYPE_ONEBYTE == addressType - * l | children address, 1 byte - * d | ELSIF 10 = FLAG_GROUP_ADDRESS_TYPE_TWOBYTES == addressType - * r | children address, 2 bytes - * e | ELSE // 11 = FLAG_GROUP_ADDRESS_TYPE_THREEBYTES = addressType - * n | children address, 3 bytes - * A | END - * d - * dress - * - * | IF FLAG_IS_TERMINAL && FLAG_HAS_SHORTCUT_TARGETS - * | shortcut string list - * | IF FLAG_IS_TERMINAL && FLAG_HAS_BIGRAMS - * | bigrams address list - * - * Char format is: - * 1 byte = bbbbbbbb match - * case 000xxxxx: xxxxx << 16 + next byte << 8 + next byte - * else: if 00011111 (= 0x1F) : this is the terminator. This is a relevant choice because - * unicode code points range from 0 to 0x10FFFF, so any 3-byte value starting with - * 00011111 would be outside unicode. - * else: iso-latin-1 code - * This allows for the whole unicode range to be encoded, including chars outside of - * the BMP. Also everything in the iso-latin-1 charset is only 1 byte, except control - * characters which should never happen anyway (and still work, but take 3 bytes). - * - * bigram address list is: - * <flags> = | hasNext = 1 bit, 1 = yes, 0 = no : FLAG_ATTRIBUTE_HAS_NEXT - * | addressSign = 1 bit, : FLAG_ATTRIBUTE_OFFSET_NEGATIVE - * | 1 = must take -address, 0 = must take +address - * | xx : mask with MASK_ATTRIBUTE_ADDRESS_TYPE - * | addressFormat = 2 bits, 00 = unused : FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE - * | 01 = 1 byte : FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE - * | 10 = 2 bytes : FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES - * | 11 = 3 bytes : FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES - * | 4 bits : frequency : mask with FLAG_ATTRIBUTE_FREQUENCY - * <address> | IF (01 == FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE == addressFormat) - * | read 1 byte, add top 4 bits - * | ELSIF (10 == FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES == addressFormat) - * | read 2 bytes, add top 4 bits - * | ELSE // 11 == FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES == addressFormat - * | read 3 bytes, add top 4 bits - * | END - * | if (FLAG_ATTRIBUTE_OFFSET_NEGATIVE) then address = -address - * if (FLAG_ATTRIBUTE_HAS_NEXT) goto bigram_and_shortcut_address_list_is - * - * shortcut string list is: - * <byte size> = GROUP_SHORTCUT_LIST_SIZE_SIZE bytes, big-endian: size of the list, in bytes. - * <flags> = | hasNext = 1 bit, 1 = yes, 0 = no : FLAG_ATTRIBUTE_HAS_NEXT - * | reserved = 3 bits, must be 0 - * | 4 bits : frequency : mask with FLAG_ATTRIBUTE_FREQUENCY - * <shortcut> = | string of characters at the char format described above, with the terminator - * | used to signal the end of the string. - * if (FLAG_ATTRIBUTE_HAS_NEXT goto flags - */ - - private static final int VERSION_1_MAGIC_NUMBER = 0x78B1; - 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; - private static final int FIRST_VERSION_WITH_HEADER_SIZE = 2; - - // These options need to be the same numeric values as the one in the native reading code. - private static final int GERMAN_UMLAUT_PROCESSING_FLAG = 0x1; - private static final int FRENCH_LIGATURE_PROCESSING_FLAG = 0x4; - private static final int CONTAINS_BIGRAMS_FLAG = 0x8; - - // TODO: Make this value adaptative to content data, store it in the header, and - // use it in the reading code. - private static final int MAX_WORD_LENGTH = 48; - - private static final int MASK_GROUP_ADDRESS_TYPE = 0xC0; - private static final int FLAG_GROUP_ADDRESS_TYPE_NOADDRESS = 0x00; - private static final int FLAG_GROUP_ADDRESS_TYPE_ONEBYTE = 0x40; - private static final int FLAG_GROUP_ADDRESS_TYPE_TWOBYTES = 0x80; - private static final int FLAG_GROUP_ADDRESS_TYPE_THREEBYTES = 0xC0; - - private static final int FLAG_HAS_MULTIPLE_CHARS = 0x20; - - 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_ATTRIBUTE_HAS_NEXT = 0x80; - private static final int FLAG_ATTRIBUTE_OFFSET_NEGATIVE = 0x40; - private static final int MASK_ATTRIBUTE_ADDRESS_TYPE = 0x30; - private static final int FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE = 0x10; - private static final int FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES = 0x20; - private static final int FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES = 0x30; - private static final int FLAG_ATTRIBUTE_FREQUENCY = 0x0F; - - private static final int GROUP_CHARACTERS_TERMINATOR = 0x1F; - - private static final int GROUP_TERMINATOR_SIZE = 1; - private static final int GROUP_FLAGS_SIZE = 1; - private static final int GROUP_FREQUENCY_SIZE = 1; - private static final int GROUP_MAX_ADDRESS_SIZE = 3; - private static final int GROUP_ATTRIBUTE_FLAGS_SIZE = 1; - private static final int GROUP_ATTRIBUTE_MAX_ADDRESS_SIZE = 3; - private static final int GROUP_SHORTCUT_LIST_SIZE_SIZE = 2; - - private static final int NO_CHILDREN_ADDRESS = Integer.MIN_VALUE; - private static final int INVALID_CHARACTER = -1; - - private static final int MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT = 0x7F; // 127 - private static final int MAX_CHARGROUPS_IN_A_NODE = 0x7FFF; // 32767 - - private static final int MAX_TERMINAL_FREQUENCY = 255; - private static final int MAX_BIGRAM_FREQUENCY = 15; + private static final boolean DBG = MakedictLog.DBG; // Arbitrary limit to how much passes we consider address size compression should // terminate in. At the time of this writing, our largest dictionary completes @@ -188,6 +55,60 @@ 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 void put(final byte b); + } + + 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); + } + + @Override + public void put(final byte b) { + mBuffer.put(b); + } + } + /** * A class grouping utility function for our specific character encoding. */ @@ -199,7 +120,7 @@ public class BinaryDictInputOutput { /** * Helper method to find out whether this code fits on one byte */ - private static boolean fitsOnOneByte(int character) { + private static boolean fitsOnOneByte(final int character) { return character >= MINIMAL_ONE_BYTE_CHARACTER_VALUE && character <= MAXIMAL_ONE_BYTE_CHARACTER_VALUE; } @@ -221,10 +142,10 @@ public class BinaryDictInputOutput { * @param character the character code. * @return the size in binary encoded-form, either 1 or 3 bytes. */ - private static int getCharSize(int character) { + private static int getCharSize(final int character) { // See char encoding in FusionDictionary.java if (fitsOnOneByte(character)) return 1; - if (INVALID_CHARACTER == character) return 1; + if (FormatSpec.INVALID_CHARACTER == character) return 1; return 3; } @@ -282,7 +203,7 @@ public class BinaryDictInputOutput { buffer[index++] = (byte)(0xFF & codePoint); } } - buffer[index++] = GROUP_CHARACTERS_TERMINATOR; + buffer[index++] = FormatSpec.GROUP_CHARACTERS_TERMINATOR; return index - origin; } @@ -294,7 +215,7 @@ public class BinaryDictInputOutput { * @param buffer the ByteArrayOutputStream to write to. * @param word the string to write. */ - private static void writeString(ByteArrayOutputStream buffer, final String word) { + private static void writeString(final ByteArrayOutputStream buffer, final String word) { final int length = word.length(); for (int i = 0; i < length; i = word.offsetByCodePoints(i, 1)) { final int codePoint = word.codePointAt(i); @@ -306,16 +227,16 @@ public class BinaryDictInputOutput { buffer.write((byte) (0xFF & codePoint)); } } - buffer.write(GROUP_CHARACTERS_TERMINATOR); + buffer.write(FormatSpec.GROUP_CHARACTERS_TERMINATOR); } /** - * Reads a string from a ByteBuffer. 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 ByteBuffer buffer) { + private static String readString(final FusionDictionaryBufferInterface buffer) { final StringBuilder s = new StringBuilder(); int character = readChar(buffer); - while (character != INVALID_CHARACTER) { + while (character != FormatSpec.INVALID_CHARACTER) { s.appendCodePoint(character); character = readChar(buffer); } @@ -323,19 +244,21 @@ public class BinaryDictInputOutput { } /** - * Reads a character from the ByteBuffer. + * Reads a character from the buffer. * * This follows the character format documented earlier in this source file. * * @param buffer the buffer, positioned over an encoded character. * @return the character code. */ - private static int readChar(final ByteBuffer buffer) { - int character = readUnsignedByte(buffer); + private static int readChar(final FusionDictionaryBufferInterface buffer) { + int character = buffer.readUnsignedByte(); if (!fitsOnOneByte(character)) { - if (GROUP_CHARACTERS_TERMINATOR == character) return INVALID_CHARACTER; + if (FormatSpec.GROUP_CHARACTERS_TERMINATOR == character) { + return FormatSpec.INVALID_CHARACTER; + } character <<= 16; - character += readUnsignedShort(buffer); + character += buffer.readUnsignedShort(); } return character; } @@ -350,9 +273,9 @@ public class BinaryDictInputOutput { * @param group the group * @return the size of the char array, including the terminator if any */ - private static int getGroupCharactersSize(CharGroup group) { + private static int getGroupCharactersSize(final CharGroup group) { int size = CharEncoding.getCharArraySize(group.mChars); - if (group.hasSeveralChars()) size += GROUP_TERMINATOR_SIZE; + if (group.hasSeveralChars()) size += FormatSpec.GROUP_TERMINATOR_SIZE; return size; } @@ -362,13 +285,14 @@ public class BinaryDictInputOutput { * @return the size of the group count, either 1 or 2 bytes. */ private static int getGroupCountSize(final int count) { - if (MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT >= count) { + if (FormatSpec.MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT >= count) { return 1; - } else if (MAX_CHARGROUPS_IN_A_NODE >= count) { + } else if (FormatSpec.MAX_CHARGROUPS_IN_A_NODE >= count) { return 2; } else { - throw new RuntimeException("Can't have more than " + MAX_CHARGROUPS_IN_A_NODE - + " groups in a node (found " + count +")"); + throw new RuntimeException("Can't have more than " + + FormatSpec.MAX_CHARGROUPS_IN_A_NODE + " groups in a node (found " + count + + ")"); } } @@ -385,14 +309,14 @@ public class BinaryDictInputOutput { * Compute the size of a shortcut in bytes. */ private static int getShortcutSize(final WeightedString shortcut) { - int size = GROUP_ATTRIBUTE_FLAGS_SIZE; + int size = FormatSpec.GROUP_ATTRIBUTE_FLAGS_SIZE; final String word = shortcut.mWord; final int length = word.length(); for (int i = 0; i < length; i = word.offsetByCodePoints(i, 1)) { final int codePoint = word.codePointAt(i); size += CharEncoding.getCharSize(codePoint); } - size += GROUP_TERMINATOR_SIZE; + size += FormatSpec.GROUP_TERMINATOR_SIZE; return size; } @@ -404,7 +328,7 @@ public class BinaryDictInputOutput { */ private static int getShortcutListSize(final ArrayList<WeightedString> shortcutList) { if (null == shortcutList) return 0; - int size = GROUP_SHORTCUT_LIST_SIZE_SIZE; + int size = FormatSpec.GROUP_SHORTCUT_LIST_SIZE_SIZE; for (final WeightedString shortcut : shortcutList) { size += getShortcutSize(shortcut); } @@ -415,16 +339,18 @@ public class BinaryDictInputOutput { * Compute the maximum size of a CharGroup, assuming 3-byte addresses for everything. * * @param group the CharGroup to compute the size of. + * @param options file format options. * @return the maximum size of the group. */ - private static int getCharGroupMaximumSize(CharGroup group) { - int size = getGroupCharactersSize(group) + GROUP_FLAGS_SIZE; + private static int getCharGroupMaximumSize(final CharGroup group, final FormatOptions options) { + int size = getGroupHeaderSize(group, options); // If terminal, one byte for the frequency - if (group.isTerminal()) size += GROUP_FREQUENCY_SIZE; - size += GROUP_MAX_ADDRESS_SIZE; // For children address + if (group.isTerminal()) size += FormatSpec.GROUP_FREQUENCY_SIZE; + size += FormatSpec.GROUP_MAX_ADDRESS_SIZE; // For children address size += getShortcutListSize(group.mShortcutTargets); if (null != group.mBigrams) { - size += (GROUP_ATTRIBUTE_FLAGS_SIZE + GROUP_ATTRIBUTE_MAX_ADDRESS_SIZE) + size += (FormatSpec.GROUP_ATTRIBUTE_FLAGS_SIZE + + FormatSpec.GROUP_ATTRIBUTE_MAX_ADDRESS_SIZE) * group.mBigrams.size(); } return size; @@ -435,11 +361,12 @@ public class BinaryDictInputOutput { * it in the 'actualSize' member of the node. * * @param node the node to compute the maximum size of. + * @param options file format options. */ - private static void setNodeMaximumSize(Node node) { + private static void setNodeMaximumSize(final Node node, final FormatOptions options) { int size = getGroupCountSize(node); for (CharGroup g : node.mData) { - final int groupSize = getCharGroupMaximumSize(g); + final int groupSize = getCharGroupMaximumSize(g, options); g.mCachedSize = groupSize; size += groupSize; } @@ -449,8 +376,31 @@ public class BinaryDictInputOutput { /** * Helper method to hide the actual value of the no children address. */ - private static boolean hasChildrenAddress(int address) { - return NO_CHILDREN_ADDRESS != address; + private static boolean hasChildrenAddress(final int address) { + return FormatSpec.NO_CHILDREN_ADDRESS != address; + } + + /** + * Helper method to check whether the CharGroup has a parent address. + */ + private static boolean hasParentAddress(final FormatOptions options) { + return options.mVersion >= FormatSpec.FIRST_VERSION_WITH_PARENT_ADDRESS + && options.mHasParentAddress; + } + + /** + * Compute the size of the header (flag + [parent address] + characters size) of a CharGroup. + * + * @param group the group of which to compute the size of the header + * @param options file format options. + */ + private static int getGroupHeaderSize(final CharGroup group, final FormatOptions options) { + if (hasParentAddress(options)) { + return FormatSpec.GROUP_FLAGS_SIZE + FormatSpec.PARENT_ADDRESS_SIZE + + getGroupCharactersSize(group); + } else { + return FormatSpec.GROUP_FLAGS_SIZE + getGroupCharactersSize(group); + } } /** @@ -463,7 +413,7 @@ public class BinaryDictInputOutput { * @param address the address * @return the byte size. */ - private static int getByteSize(int address) { + private static int getByteSize(final int address) { assert(address < 0x1000000); if (!hasChildrenAddress(address)) { return 0; @@ -479,14 +429,14 @@ public class BinaryDictInputOutput { // This method is responsible for finding a nice ordering of the nodes that favors run-time // cache performance and dictionary size. - /* package for tests */ static ArrayList<Node> flattenTree(Node root) { + /* package for tests */ static ArrayList<Node> flattenTree(final Node root) { final int treeSize = FusionDictionary.countCharGroups(root); MakedictLog.i("Counted nodes : " + treeSize); final ArrayList<Node> flatTree = new ArrayList<Node>(treeSize); return flattenTreeInner(flatTree, root); } - private static ArrayList<Node> flattenTreeInner(ArrayList<Node> list, Node node) { + private static ArrayList<Node> flattenTreeInner(final ArrayList<Node> list, final Node node) { // Removing the node is necessary if the tails are merged, because we would then // add the same node several times when we only want it once. A number of places in // the code also depends on any node being only once in the list. @@ -536,9 +486,11 @@ public class BinaryDictInputOutput { * * @param node the node to compute the size of. * @param dict the dictionary in which the word/attributes are to be found. + * @param formatOptions file format options. * @return false if none of the cached addresses inside the node changed, true otherwise. */ - private static boolean computeActualNodeSize(Node node, FusionDictionary dict) { + private static boolean computeActualNodeSize(final Node node, final FusionDictionary dict, + final FormatOptions formatOptions) { boolean changed = false; int size = getGroupCountSize(node); for (CharGroup group : node.mData) { @@ -546,21 +498,24 @@ public class BinaryDictInputOutput { changed = true; group.mCachedAddress = node.mCachedAddress + size; } - int groupSize = GROUP_FLAGS_SIZE + getGroupCharactersSize(group); - if (group.isTerminal()) groupSize += GROUP_FREQUENCY_SIZE; + int groupSize = getGroupHeaderSize(group, formatOptions); + if (group.isTerminal()) groupSize += FormatSpec.GROUP_FREQUENCY_SIZE; if (null != group.mChildren) { - final int offsetBasePoint= groupSize + node.mCachedAddress + size; + final int offsetBasePoint = groupSize + node.mCachedAddress + size; final int offset = group.mChildren.mCachedAddress - offsetBasePoint; + // assign my address to children's parent address + group.mChildren.mCachedParentAddress = group.mCachedAddress + - group.mChildren.mCachedAddress; groupSize += getByteSize(offset); } groupSize += getShortcutListSize(group.mShortcutTargets); if (null != group.mBigrams) { for (WeightedString bigram : group.mBigrams) { final int offsetBasePoint = groupSize + node.mCachedAddress + size - + GROUP_FLAGS_SIZE; + + FormatSpec.GROUP_FLAGS_SIZE; final int addressOfBigram = findAddressOfWord(dict, bigram.mWord); final int offset = addressOfBigram - offsetBasePoint; - groupSize += getByteSize(offset) + GROUP_FLAGS_SIZE; + groupSize += getByteSize(offset) + FormatSpec.GROUP_FLAGS_SIZE; } } group.mCachedSize = groupSize; @@ -579,7 +534,7 @@ public class BinaryDictInputOutput { * @param flatNodes the array of nodes. * @return the byte size of the entire stack. */ - private static int stackNodes(ArrayList<Node> flatNodes) { + private static int stackNodes(final ArrayList<Node> flatNodes) { int nodeOffset = 0; for (Node n : flatNodes) { n.mCachedAddress = nodeOffset; @@ -609,12 +564,13 @@ public class BinaryDictInputOutput { * * @param dict the dictionary * @param flatNodes the ordered array of nodes + * @param formatOptions file format options. * @return the same array it was passed. The nodes have been updated for address and size. */ - private static ArrayList<Node> computeAddresses(FusionDictionary dict, - ArrayList<Node> flatNodes) { + private static ArrayList<Node> computeAddresses(final FusionDictionary dict, + final ArrayList<Node> flatNodes, final FormatOptions formatOptions) { // First get the worst sizes and offsets - for (Node n : flatNodes) setNodeMaximumSize(n); + for (Node n : flatNodes) setNodeMaximumSize(n, formatOptions); final int offset = stackNodes(flatNodes); MakedictLog.i("Compressing the array addresses. Original size : " + offset); @@ -626,7 +582,7 @@ public class BinaryDictInputOutput { changesDone = false; for (Node n : flatNodes) { final int oldNodeSize = n.mCachedSize; - final boolean changed = computeActualNodeSize(n, dict); + final boolean changed = computeActualNodeSize(n, dict, formatOptions); final int newNodeSize = n.mCachedSize; if (oldNodeSize < newNodeSize) throw new RuntimeException("Increased size ?!"); changesDone |= changed; @@ -654,7 +610,7 @@ public class BinaryDictInputOutput { * * @param array the array node to check */ - private static void checkFlatNodeArray(ArrayList<Node> array) { + private static void checkFlatNodeArray(final ArrayList<Node> array) { int offset = 0; int index = 0; for (Node n : array) { @@ -699,20 +655,20 @@ public class BinaryDictInputOutput { private static byte makeCharGroupFlags(final CharGroup group, final int groupAddress, final int childrenOffset) { byte flags = 0; - if (group.mChars.length > 1) flags |= FLAG_HAS_MULTIPLE_CHARS; + if (group.mChars.length > 1) flags |= FormatSpec.FLAG_HAS_MULTIPLE_CHARS; if (group.mFrequency >= 0) { - flags |= FLAG_IS_TERMINAL; + flags |= FormatSpec.FLAG_IS_TERMINAL; } if (null != group.mChildren) { switch (getByteSize(childrenOffset)) { case 1: - flags |= FLAG_GROUP_ADDRESS_TYPE_ONEBYTE; + flags |= FormatSpec.FLAG_GROUP_ADDRESS_TYPE_ONEBYTE; break; case 2: - flags |= FLAG_GROUP_ADDRESS_TYPE_TWOBYTES; + flags |= FormatSpec.FLAG_GROUP_ADDRESS_TYPE_TWOBYTES; break; case 3: - flags |= FLAG_GROUP_ADDRESS_TYPE_THREEBYTES; + flags |= FormatSpec.FLAG_GROUP_ADDRESS_TYPE_THREEBYTES; break; default: throw new RuntimeException("Node with a strange address"); @@ -722,13 +678,19 @@ public class BinaryDictInputOutput { if (DBG && 0 == group.mShortcutTargets.size()) { throw new RuntimeException("0-sized shortcut list must be null"); } - flags |= FLAG_HAS_SHORTCUT_TARGETS; + flags |= FormatSpec.FLAG_HAS_SHORTCUT_TARGETS; } if (null != group.mBigrams) { if (DBG && 0 == group.mBigrams.size()) { throw new RuntimeException("0-sized bigram list must be null"); } - flags |= FLAG_HAS_BIGRAMS; + flags |= FormatSpec.FLAG_HAS_BIGRAMS; + } + if (group.mIsNotAWord) { + flags |= FormatSpec.FLAG_IS_NOT_A_WORD; + } + if (group.mIsBlacklistEntry) { + flags |= FormatSpec.FLAG_IS_BLACKLISTED; } return flags; } @@ -745,17 +707,17 @@ public class BinaryDictInputOutput { */ private static final int makeBigramFlags(final boolean more, final int offset, int bigramFrequency, final int unigramFrequency, final String word) { - int bigramFlags = (more ? FLAG_ATTRIBUTE_HAS_NEXT : 0) - + (offset < 0 ? FLAG_ATTRIBUTE_OFFSET_NEGATIVE : 0); + int bigramFlags = (more ? FormatSpec.FLAG_ATTRIBUTE_HAS_NEXT : 0) + + (offset < 0 ? FormatSpec.FLAG_ATTRIBUTE_OFFSET_NEGATIVE : 0); switch (getByteSize(offset)) { case 1: - bigramFlags |= FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE; + bigramFlags |= FormatSpec.FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE; break; case 2: - bigramFlags |= FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES; + bigramFlags |= FormatSpec.FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES; break; case 3: - bigramFlags |= FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES; + bigramFlags |= FormatSpec.FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES; break; default: throw new RuntimeException("Strange offset size"); @@ -790,7 +752,8 @@ public class BinaryDictInputOutput { // 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); + (FormatSpec.MAX_TERMINAL_FREQUENCY - unigramFrequency) + / (1.5f + FormatSpec.MAX_BIGRAM_FREQUENCY); final float firstStepStart = 1 + unigramFrequency + (stepSize / 2.0f); final int discretizedFrequency = (int)((bigramFrequency - firstStepStart) / stepSize); // If the bigram freq is less than half-a-step higher than the unigram freq, we get -1 @@ -799,19 +762,21 @@ public class BinaryDictInputOutput { // small over-estimation that we get in this case. TODO: actually remove this bigram // if discretizedFrequency < 0. final int finalBigramFrequency = discretizedFrequency > 0 ? discretizedFrequency : 0; - bigramFlags += finalBigramFrequency & FLAG_ATTRIBUTE_FREQUENCY; + bigramFlags += finalBigramFrequency & FormatSpec.FLAG_ATTRIBUTE_FREQUENCY; return bigramFlags; } /** * Makes the 2-byte value for options flags. */ - private static final int makeOptionsValue(final FusionDictionary dictionary) { + private static final int makeOptionsValue(final FusionDictionary dictionary, + final FormatOptions formatOptions) { final DictionaryOptions options = dictionary.mOptions; final boolean hasBigrams = dictionary.hasBigrams(); - return (options.mFrenchLigatureProcessing ? FRENCH_LIGATURE_PROCESSING_FLAG : 0) - + (options.mGermanUmlautProcessing ? GERMAN_UMLAUT_PROCESSING_FLAG : 0) - + (hasBigrams ? CONTAINS_BIGRAMS_FLAG : 0); + return (options.mFrenchLigatureProcessing ? FormatSpec.FRENCH_LIGATURE_PROCESSING_FLAG : 0) + + (options.mGermanUmlautProcessing ? FormatSpec.GERMAN_UMLAUT_PROCESSING_FLAG : 0) + + (hasBigrams ? FormatSpec.CONTAINS_BIGRAMS_FLAG : 0) + + (formatOptions.mHasParentAddress ? FormatSpec.HAS_PARENT_ADDRESS : 0); } /** @@ -822,7 +787,8 @@ public class BinaryDictInputOutput { * @return the flags */ private static final int makeShortcutFlags(final boolean more, final int frequency) { - return (more ? FLAG_ATTRIBUTE_HAS_NEXT : 0) + (frequency & FLAG_ATTRIBUTE_FREQUENCY); + return (more ? FormatSpec.FLAG_ATTRIBUTE_HAS_NEXT : 0) + + (frequency & FormatSpec.FLAG_ATTRIBUTE_FREQUENCY); } /** @@ -834,13 +800,16 @@ public class BinaryDictInputOutput { * @param dict the dictionary the node is a part of (for relative offsets). * @param buffer the memory buffer to write to. * @param node the node to write. + * @param formatOptions file format options. * @return the address of the END of the node. */ - private static int writePlacedNode(FusionDictionary dict, byte[] buffer, Node node) { + private static int writePlacedNode(final FusionDictionary dict, byte[] buffer, + final Node node, final FormatOptions formatOptions) { int index = node.mCachedAddress; final int groupCount = node.mData.size(); final int countSize = getGroupCountSize(node); + final int parentAddress = node.mCachedParentAddress; if (1 == countSize) { buffer[index++] = (byte)groupCount; } else if (2 == countSize) { @@ -857,20 +826,38 @@ public class BinaryDictInputOutput { if (index != group.mCachedAddress) throw new RuntimeException("Bug: write index is not " + "the same as the cached address of the group : " + index + " <> " + group.mCachedAddress); - groupAddress += GROUP_FLAGS_SIZE + getGroupCharactersSize(group); + groupAddress += getGroupHeaderSize(group, formatOptions); // Sanity checks. - if (DBG && group.mFrequency > MAX_TERMINAL_FREQUENCY) { - throw new RuntimeException("A node has a frequency > " + MAX_TERMINAL_FREQUENCY + if (DBG && group.mFrequency > FormatSpec.MAX_TERMINAL_FREQUENCY) { + throw new RuntimeException("A node has a frequency > " + + FormatSpec.MAX_TERMINAL_FREQUENCY + " : " + group.mFrequency); } - if (group.mFrequency >= 0) groupAddress += GROUP_FREQUENCY_SIZE; + if (group.mFrequency >= 0) groupAddress += FormatSpec.GROUP_FREQUENCY_SIZE; final int childrenOffset = null == group.mChildren - ? NO_CHILDREN_ADDRESS : group.mChildren.mCachedAddress - groupAddress; + ? FormatSpec.NO_CHILDREN_ADDRESS + : group.mChildren.mCachedAddress - groupAddress; byte flags = makeCharGroupFlags(group, groupAddress, childrenOffset); buffer[index++] = flags; + + if (hasParentAddress(formatOptions)) { + if (parentAddress == FormatSpec.NO_PARENT_ADDRESS) { + // this node is the root node. + buffer[index] = buffer[index + 1] = buffer[index + 2] = 0; + } else { + // write parent address. (version 3) + final int actualParentAddress = Math.abs(parentAddress + + (node.mCachedAddress - group.mCachedAddress)); + buffer[index] = (byte)((actualParentAddress >> 16) & 0xFF); + buffer[index + 1] = (byte)((actualParentAddress >> 8) & 0xFF); + buffer[index + 2] = (byte)(actualParentAddress & 0xFF); + } + index += 3; + } + index = CharEncoding.writeCharArray(group.mChars, buffer, index); if (group.hasSeveralChars()) { - buffer[index++] = GROUP_CHARACTERS_TERMINATOR; + buffer[index++] = FormatSpec.GROUP_CHARACTERS_TERMINATOR; } if (group.mFrequency >= 0) { buffer[index++] = (byte) group.mFrequency; @@ -882,8 +869,8 @@ public class BinaryDictInputOutput { // Write shortcuts if (null != group.mShortcutTargets) { final int indexOfShortcutByteSize = index; - index += GROUP_SHORTCUT_LIST_SIZE_SIZE; - groupAddress += GROUP_SHORTCUT_LIST_SIZE_SIZE; + index += FormatSpec.GROUP_SHORTCUT_LIST_SIZE_SIZE; + groupAddress += FormatSpec.GROUP_SHORTCUT_LIST_SIZE_SIZE; final Iterator<WeightedString> shortcutIterator = group.mShortcutTargets.iterator(); while (shortcutIterator.hasNext()) { final WeightedString target = shortcutIterator.next(); @@ -992,10 +979,10 @@ public class BinaryDictInputOutput { * * @param destination the stream to write the binary data to. * @param dict the dictionary to write. - * @param version the version of the format to write, currently either 1 or 2. + * @param formatOptions file format options. */ public static void writeDictionaryBinary(final OutputStream destination, - final FusionDictionary dict, final int version) + final FusionDictionary dict, final FormatOptions formatOptions) throws IOException, UnsupportedFormatException { // Addresses are limited to 3 bytes, but since addresses can be relative to each node, the @@ -1004,36 +991,39 @@ public class BinaryDictInputOutput { // does not have a size limit, each node must still be within 16MB of all its children and // parents. As long as this is ensured, the dictionary file may grow to any size. - if (version < MINIMUM_SUPPORTED_VERSION || version > MAXIMUM_SUPPORTED_VERSION) { + final int version = formatOptions.mVersion; + if (version < FormatSpec.MINIMUM_SUPPORTED_VERSION + || version > FormatSpec.MAXIMUM_SUPPORTED_VERSION) { throw new UnsupportedFormatException("Requested file format version " + version + ", but this implementation only supports versions " - + MINIMUM_SUPPORTED_VERSION + " through " + MAXIMUM_SUPPORTED_VERSION); + + FormatSpec.MINIMUM_SUPPORTED_VERSION + " through " + + FormatSpec.MAXIMUM_SUPPORTED_VERSION); } ByteArrayOutputStream headerBuffer = new ByteArrayOutputStream(256); // The magic number in big-endian order. - if (version >= FIRST_VERSION_WITH_HEADER_SIZE) { + if (version >= FormatSpec.FIRST_VERSION_WITH_HEADER_SIZE) { // Magic number for version 2+. - headerBuffer.write((byte) (0xFF & (VERSION_2_MAGIC_NUMBER >> 24))); - headerBuffer.write((byte) (0xFF & (VERSION_2_MAGIC_NUMBER >> 16))); - headerBuffer.write((byte) (0xFF & (VERSION_2_MAGIC_NUMBER >> 8))); - headerBuffer.write((byte) (0xFF & VERSION_2_MAGIC_NUMBER)); + headerBuffer.write((byte) (0xFF & (FormatSpec.VERSION_2_MAGIC_NUMBER >> 24))); + headerBuffer.write((byte) (0xFF & (FormatSpec.VERSION_2_MAGIC_NUMBER >> 16))); + headerBuffer.write((byte) (0xFF & (FormatSpec.VERSION_2_MAGIC_NUMBER >> 8))); + headerBuffer.write((byte) (0xFF & FormatSpec.VERSION_2_MAGIC_NUMBER)); // Dictionary version. headerBuffer.write((byte) (0xFF & (version >> 8))); headerBuffer.write((byte) (0xFF & version)); } else { // Magic number for version 1. - headerBuffer.write((byte) (0xFF & (VERSION_1_MAGIC_NUMBER >> 8))); - headerBuffer.write((byte) (0xFF & VERSION_1_MAGIC_NUMBER)); + headerBuffer.write((byte) (0xFF & (FormatSpec.VERSION_1_MAGIC_NUMBER >> 8))); + headerBuffer.write((byte) (0xFF & FormatSpec.VERSION_1_MAGIC_NUMBER)); // Dictionary version. headerBuffer.write((byte) (0xFF & version)); } // Options flags - final int options = makeOptionsValue(dict); + final int options = makeOptionsValue(dict, formatOptions); headerBuffer.write((byte) (0xFF & (options >> 8))); headerBuffer.write((byte) (0xFF & options)); - if (version >= FIRST_VERSION_WITH_HEADER_SIZE) { + if (version >= FormatSpec.FIRST_VERSION_WITH_HEADER_SIZE) { final int headerSizeOffset = headerBuffer.size(); // Placeholder to be written later with header size. for (int i = 0; i < 4; ++i) { @@ -1064,20 +1054,20 @@ public class BinaryDictInputOutput { ArrayList<Node> flatNodes = flattenTree(dict.mRoot); MakedictLog.i("Computing addresses..."); - computeAddresses(dict, flatNodes); + computeAddresses(dict, flatNodes, formatOptions); MakedictLog.i("Checking array..."); if (DBG) checkFlatNodeArray(flatNodes); // Create a buffer that matches the final dictionary size. final Node lastNode = flatNodes.get(flatNodes.size() - 1); - final int bufferSize =(lastNode.mCachedAddress + lastNode.mCachedSize); + final int bufferSize = lastNode.mCachedAddress + lastNode.mCachedSize; final byte[] buffer = new byte[bufferSize]; int index = 0; MakedictLog.i("Writing file..."); int dataEndOffset = 0; for (Node n : flatNodes) { - dataEndOffset = writePlacedNode(dict, buffer, n); + dataEndOffset = writePlacedNode(dict, buffer, n, formatOptions); } if (DBG) showStatistics(flatNodes); @@ -1092,113 +1082,127 @@ public class BinaryDictInputOutput { // Input methods: Read a binary dictionary to memory. // readDictionaryBinary is the public entry point for them. - static final int[] characterBuffer = new int[MAX_WORD_LENGTH]; - private static CharGroupInfo readCharGroup(final ByteBuffer buffer, - final int originalGroupAddress) { + private static final int[] CHARACTER_BUFFER = new int[FormatSpec.MAX_WORD_LENGTH]; + private static CharGroupInfo readCharGroup(final FusionDictionaryBufferInterface buffer, + final int originalGroupAddress, final FormatOptions options) { int addressPointer = originalGroupAddress; - final int flags = readUnsignedByte(buffer); + final int flags = buffer.readUnsignedByte(); ++addressPointer; + + final int parentAddress; + if (hasParentAddress(options)) { + // read the parent address. (version 3) + parentAddress = -buffer.readUnsignedInt24(); + addressPointer += 3; + } else { + parentAddress = FormatSpec.NO_PARENT_ADDRESS; + } + final int characters[]; - if (0 != (flags & FLAG_HAS_MULTIPLE_CHARS)) { + if (0 != (flags & FormatSpec.FLAG_HAS_MULTIPLE_CHARS)) { int index = 0; int character = CharEncoding.readChar(buffer); addressPointer += CharEncoding.getCharSize(character); while (-1 != character) { - characterBuffer[index++] = character; + // FusionDictionary is making sure that the length of the word is smaller than + // MAX_WORD_LENGTH. + // So we'll never write past the end of CHARACTER_BUFFER. + CHARACTER_BUFFER[index++] = character; character = CharEncoding.readChar(buffer); addressPointer += CharEncoding.getCharSize(character); } - characters = Arrays.copyOfRange(characterBuffer, 0, index); + characters = Arrays.copyOfRange(CHARACTER_BUFFER, 0, index); } else { final int character = CharEncoding.readChar(buffer); addressPointer += CharEncoding.getCharSize(character); characters = new int[] { character }; } final int frequency; - if (0 != (FLAG_IS_TERMINAL & flags)) { + if (0 != (FormatSpec.FLAG_IS_TERMINAL & flags)) { ++addressPointer; - frequency = readUnsignedByte(buffer); + 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 += readUnsignedByte(buffer); + switch (flags & FormatSpec.MASK_GROUP_ADDRESS_TYPE) { + case FormatSpec.FLAG_GROUP_ADDRESS_TYPE_ONEBYTE: + childrenAddress += buffer.readUnsignedByte(); addressPointer += 1; break; - case FLAG_GROUP_ADDRESS_TYPE_TWOBYTES: - childrenAddress += readUnsignedShort(buffer); + case FormatSpec.FLAG_GROUP_ADDRESS_TYPE_TWOBYTES: + childrenAddress += buffer.readUnsignedShort(); addressPointer += 2; break; - case FLAG_GROUP_ADDRESS_TYPE_THREEBYTES: - childrenAddress += readUnsignedInt24(buffer); + case FormatSpec.FLAG_GROUP_ADDRESS_TYPE_THREEBYTES: + childrenAddress += buffer.readUnsignedInt24(); addressPointer += 3; break; - case FLAG_GROUP_ADDRESS_TYPE_NOADDRESS: + case FormatSpec.FLAG_GROUP_ADDRESS_TYPE_NOADDRESS: default: - childrenAddress = NO_CHILDREN_ADDRESS; + childrenAddress = FormatSpec.NO_CHILDREN_ADDRESS; break; } ArrayList<WeightedString> shortcutTargets = null; - if (0 != (flags & FLAG_HAS_SHORTCUT_TARGETS)) { + if (0 != (flags & FormatSpec.FLAG_HAS_SHORTCUT_TARGETS)) { final int pointerBefore = buffer.position(); shortcutTargets = new ArrayList<WeightedString>(); - buffer.getShort(); // Skip the size + buffer.readUnsignedShort(); // Skip the size while (true) { - final int targetFlags = readUnsignedByte(buffer); + 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; + targetFlags & FormatSpec.FLAG_ATTRIBUTE_FREQUENCY)); + if (0 == (targetFlags & FormatSpec.FLAG_ATTRIBUTE_HAS_NEXT)) break; } addressPointer += buffer.position() - pointerBefore; } ArrayList<PendingAttribute> bigrams = null; - if (0 != (flags & FLAG_HAS_BIGRAMS)) { + if (0 != (flags & FormatSpec.FLAG_HAS_BIGRAMS)) { bigrams = new ArrayList<PendingAttribute>(); while (true) { - final int bigramFlags = readUnsignedByte(buffer); + final int bigramFlags = buffer.readUnsignedByte(); ++addressPointer; - final int sign = 0 == (bigramFlags & FLAG_ATTRIBUTE_OFFSET_NEGATIVE) ? 1 : -1; + final int sign = 0 == (bigramFlags & FormatSpec.FLAG_ATTRIBUTE_OFFSET_NEGATIVE) + ? 1 : -1; int bigramAddress = addressPointer; - switch (bigramFlags & MASK_ATTRIBUTE_ADDRESS_TYPE) { - case FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE: - bigramAddress += sign * readUnsignedByte(buffer); + switch (bigramFlags & FormatSpec.MASK_ATTRIBUTE_ADDRESS_TYPE) { + case FormatSpec.FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE: + bigramAddress += sign * buffer.readUnsignedByte(); addressPointer += 1; break; - case FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES: - bigramAddress += sign * readUnsignedShort(buffer); + case FormatSpec.FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES: + bigramAddress += sign * buffer.readUnsignedShort(); addressPointer += 2; break; - case FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES: - final int offset = (readUnsignedByte(buffer) << 16) - + readUnsignedShort(buffer); + case FormatSpec.FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES: + final int offset = (buffer.readUnsignedByte() << 16) + + buffer.readUnsignedShort(); bigramAddress += sign * offset; addressPointer += 3; break; default: throw new RuntimeException("Has bigrams with no address"); } - bigrams.add(new PendingAttribute(bigramFlags & FLAG_ATTRIBUTE_FREQUENCY, + bigrams.add(new PendingAttribute(bigramFlags & FormatSpec.FLAG_ATTRIBUTE_FREQUENCY, bigramAddress)); - if (0 == (bigramFlags & FLAG_ATTRIBUTE_HAS_NEXT)) break; + if (0 == (bigramFlags & FormatSpec.FLAG_ATTRIBUTE_HAS_NEXT)) break; } } return new CharGroupInfo(originalGroupAddress, addressPointer, flags, characters, frequency, - childrenAddress, shortcutTargets, bigrams); + parentAddress, childrenAddress, shortcutTargets, bigrams); } /** * Reads and returns the char group count out of a buffer and forwards the pointer. */ - private static int readCharGroupCount(final ByteBuffer buffer) { - final int msb = readUnsignedByte(buffer); - if (MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT >= msb) { + private static int readCharGroupCount(final FusionDictionaryBufferInterface buffer) { + final int msb = buffer.readUnsignedByte(); + if (FormatSpec.MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT >= msb) { return msb; } else { - return ((MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT & msb) << 8) - + readUnsignedByte(buffer); + return ((FormatSpec.MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT & msb) << 8) + + buffer.readUnsignedByte(); } } @@ -1213,13 +1217,56 @@ public class BinaryDictInputOutput { * @param buffer the buffer to read from. * @param headerSize the size of the header. * @param address the address to seek. + * @param formatOptions file format options. * @return the word, as a string. */ - private static String getWordAtAddress(final ByteBuffer buffer, final int headerSize, - final int address) { + private static String getWordAtAddress(final FusionDictionaryBufferInterface buffer, + final int headerSize, final int address, final FormatOptions formatOptions) { final String cachedString = wordCache.get(address); if (null != cachedString) return cachedString; + + final String result; final int originalPointer = buffer.position(); + + if (hasParentAddress(formatOptions)) { + result = getWordAtAddressWithParentAddress(buffer, headerSize, address, formatOptions); + } else { + result = getWordAtAddressWithoutParentAddress(buffer, headerSize, address, + formatOptions); + } + + wordCache.put(address, result); + buffer.position(originalPointer); + return result; + } + + private static int[] sGetWordBuffer = new int[FormatSpec.MAX_WORD_LENGTH]; + private static String getWordAtAddressWithParentAddress( + final FusionDictionaryBufferInterface buffer, final int headerSize, final int address, + final FormatOptions options) { + final StringBuilder builder = new StringBuilder(); + + int currentAddress = address; + int index = FormatSpec.MAX_WORD_LENGTH - 1; + // the length of the path from the root to the leaf is limited by MAX_WORD_LENGTH + for (int count = 0; count < FormatSpec.MAX_WORD_LENGTH; ++count) { + buffer.position(currentAddress + headerSize); + final CharGroupInfo currentInfo = readCharGroup(buffer, currentAddress, options); + for (int i = 0; i < currentInfo.mCharacters.length; ++i) { + sGetWordBuffer[index--] = + currentInfo.mCharacters[currentInfo.mCharacters.length - i - 1]; + } + + if (currentInfo.mParentAddress == FormatSpec.NO_PARENT_ADDRESS) break; + currentAddress = currentInfo.mParentAddress + currentInfo.mOriginalAddress; + } + + return new String(sGetWordBuffer, index + 1, FormatSpec.MAX_WORD_LENGTH - index - 1); + } + + private static String getWordAtAddressWithoutParentAddress( + final FusionDictionaryBufferInterface buffer, final int headerSize, final int address, + final FormatOptions options) { buffer.position(headerSize); final int count = readCharGroupCount(buffer); int groupOffset = getGroupCountSize(count); @@ -1228,7 +1275,7 @@ public class BinaryDictInputOutput { CharGroupInfo last = null; for (int i = count - 1; i >= 0; --i) { - CharGroupInfo info = readCharGroup(buffer, groupOffset); + CharGroupInfo info = readCharGroup(buffer, groupOffset, options); groupOffset = info.mEndAddress; if (info.mOriginalAddress == address) { builder.append(new String(info.mCharacters, 0, info.mCharacters.length)); @@ -1241,7 +1288,7 @@ public class BinaryDictInputOutput { builder.append(new String(last.mCharacters, 0, last.mCharacters.length)); buffer.position(last.mChildrenAddress + headerSize); groupOffset = last.mChildrenAddress + 1; - i = readUnsignedByte(buffer); + i = buffer.readUnsignedByte(); last = null; continue; } @@ -1251,21 +1298,19 @@ public class BinaryDictInputOutput { builder.append(new String(last.mCharacters, 0, last.mCharacters.length)); buffer.position(last.mChildrenAddress + headerSize); groupOffset = last.mChildrenAddress + 1; - i = readUnsignedByte(buffer); + i = buffer.readUnsignedByte(); last = null; continue; } } - 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. * @@ -1273,24 +1318,26 @@ public class BinaryDictInputOutput { * @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. + * @param options file format options. * @return the read node with all his children already read. */ - private static Node readNode(final ByteBuffer buffer, final int headerSize, - final Map<Integer, Node> reverseNodeMap, final Map<Integer, CharGroup> reverseGroupMap) + private static Node readNode(final FusionDictionaryBufferInterface buffer, final int headerSize, + final Map<Integer, Node> reverseNodeMap, final Map<Integer, CharGroup> reverseGroupMap, + final FormatOptions options) throws IOException { 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(buffer, groupOffset); + CharGroupInfo info = readCharGroup(buffer, groupOffset, options); 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( - buffer, headerSize, bigram.mAddress); + buffer, headerSize, bigram.mAddress, options); bigrams.add(new WeightedString(word, bigram.mFrequency)); } } @@ -1300,16 +1347,18 @@ public class BinaryDictInputOutput { final int currentPosition = buffer.position(); buffer.position(info.mChildrenAddress + headerSize); children = readNode( - buffer, headerSize, reverseNodeMap, reverseGroupMap); + buffer, headerSize, reverseNodeMap, reverseGroupMap, options); buffer.position(currentPosition); } nodeContents.add( - new CharGroup(info.mCharacters, shortcutTargets, - bigrams, info.mFrequency, children)); + new CharGroup(info.mCharacters, shortcutTargets, bigrams, info.mFrequency, + 0 != (info.mFlags & FormatSpec.FLAG_IS_NOT_A_WORD), + 0 != (info.mFlags & FormatSpec.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 & FormatSpec.FLAG_IS_NOT_A_WORD), + 0 != (info.mFlags & FormatSpec.FLAG_IS_BLACKLISTED))); } groupOffset = info.mEndAddress; } @@ -1319,86 +1368,220 @@ 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, + final FormatOptions formatOptions) { + int[] pushedChars = new int[FormatSpec.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, formatOptions); + 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 FileHeader header = readHeader(buffer); + readUnigramsAndBigramsBinaryInner(buffer, header.mHeaderSize, words, frequencies, bigrams, + header.mFormatOptions); + } + /** * Helper function to get the binary format version from the header. * @throws IOException */ - private static int getFormatVersion(final ByteBuffer buffer) throws IOException { - final int magic_v1 = readUnsignedShort(buffer); - if (VERSION_1_MAGIC_NUMBER == magic_v1) return readUnsignedByte(buffer); - final int magic_v2 = (magic_v1 << 16) + readUnsignedShort(buffer); - if (VERSION_2_MAGIC_NUMBER == magic_v2) return readUnsignedShort(buffer); - return NOT_A_VERSION_NUMBER; + private static int getFormatVersion(final FusionDictionaryBufferInterface buffer) + throws IOException { + final int magic_v1 = buffer.readUnsignedShort(); + if (FormatSpec.VERSION_1_MAGIC_NUMBER == magic_v1) return buffer.readUnsignedByte(); + final int magic_v2 = (magic_v1 << 16) + buffer.readUnsignedShort(); + if (FormatSpec.VERSION_2_MAGIC_NUMBER == magic_v2) return buffer.readUnsignedShort(); + return FormatSpec.NOT_A_VERSION_NUMBER; + } + + /** + * Helper function to get and validate the binary format version. + * @throws UnsupportedFormatException + * @throws IOException + */ + private static int checkFormatVersion(final FusionDictionaryBufferInterface buffer) + throws IOException, UnsupportedFormatException { + final int version = getFormatVersion(buffer); + if (version < FormatSpec.MINIMUM_SUPPORTED_VERSION + || version > FormatSpec.MAXIMUM_SUPPORTED_VERSION) { + throw new UnsupportedFormatException("This file has version " + version + + ", but this implementation does not support versions above " + + FormatSpec.MAXIMUM_SUPPORTED_VERSION); + } + return version; } /** - * Reads options from a file and populate a map with their contents. + * Reads a header from a buffer. + * @param buffer the buffer to read. + * @throws IOException + * @throws UnsupportedFormatException + */ + private static FileHeader readHeader(final FusionDictionaryBufferInterface buffer) + throws IOException, UnsupportedFormatException { + final int version = checkFormatVersion(buffer); + final int optionsFlags = buffer.readUnsignedShort(); + + final HashMap<String, String> attributes = new HashMap<String, String>(); + final int headerSize; + if (version < FormatSpec.FIRST_VERSION_WITH_HEADER_SIZE) { + headerSize = buffer.position(); + } else { + headerSize = buffer.readInt(); + populateOptions(buffer, headerSize, attributes); + buffer.position(headerSize); + } + + if (headerSize < 0) { + throw new UnsupportedFormatException("header size can't be negative."); + } + + final FileHeader header = new FileHeader(headerSize, + new FusionDictionary.DictionaryOptions(attributes, + 0 != (optionsFlags & FormatSpec.GERMAN_UMLAUT_PROCESSING_FLAG), + 0 != (optionsFlags & FormatSpec.FRENCH_LIGATURE_PROCESSING_FLAG)), + new FormatOptions(version, + 0 != (optionsFlags & FormatSpec.HAS_PARENT_ADDRESS))); + return header; + } + + /** + * Reads options from a buffer and populate a map with their contents. * - * The file is read at the current file pointer, so the caller must take care the pointer + * 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 ByteBuffer buffer, final int headerSize, - final HashMap<String, String> options) { + 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 byte buffer and returns the memory representation of the dictionary. + * Reads a buffer and returns the memory representation of the dictionary. * - * This high-level method takes a binary file and reads its contents, populating a + * 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 file should be added. If it is null, a new dictionary is created. + * 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 ByteBuffer buffer, - final FusionDictionary dict) throws IOException, UnsupportedFormatException { - // Check file version - 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); - } - + public static FusionDictionary readDictionaryBinary( + final FusionDictionaryBufferInterface buffer, final FusionDictionary dict) + throws IOException, UnsupportedFormatException { // clear cache wordCache.clear(); - // Read options - final int optionsFlags = readUnsignedShort(buffer); - - final int headerSize; - final HashMap<String, String> options = new HashMap<String, String>(); - if (version < FIRST_VERSION_WITH_HEADER_SIZE) { - headerSize = buffer.position(); - } else { - headerSize = buffer.getInt(); - populateOptions(buffer, headerSize, options); - buffer.position(headerSize); - } - - if (headerSize < 0) { - throw new UnsupportedFormatException("header size can't be negative."); - } + // Read header + final FileHeader header = readHeader(buffer); Map<Integer, Node> reverseNodeMapping = new TreeMap<Integer, Node>(); Map<Integer, CharGroup> reverseGroupMapping = new TreeMap<Integer, CharGroup>(); - final Node root = readNode( - buffer, headerSize, reverseNodeMapping, reverseGroupMapping); + final Node root = readNode(buffer, header.mHeaderSize, reverseNodeMapping, + reverseGroupMapping, header.mFormatOptions); - FusionDictionary newDict = new FusionDictionary(root, - new FusionDictionary.DictionaryOptions(options, - 0 != (optionsFlags & GERMAN_UMLAUT_PROCESSING_FLAG), - 0 != (optionsFlags & FRENCH_LIGATURE_PROCESSING_FLAG))); + FusionDictionary newDict = new FusionDictionary(root, header.mDictionaryOptions); 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 @@ -1414,28 +1597,6 @@ public class BinaryDictInputOutput { } /** - * Helper function to read one byte from ByteBuffer. - */ - private static int readUnsignedByte(final ByteBuffer buffer) { - return ((int)buffer.get()) & 0xFF; - } - - /** - * Helper function to read two byte from ByteBuffer. - */ - private static int readUnsignedShort(final ByteBuffer buffer) { - return ((int)buffer.getShort()) & 0xFFFF; - } - - /** - * Helper function to read three byte from ByteBuffer. - */ - private static int readUnsignedInt24(final ByteBuffer buffer) { - final int value = readUnsignedByte(buffer) << 16; - return value + readUnsignedShort(buffer); - } - - /** * Basic test to find out whether the file is a binary dictionary or not. * * Concretely this only tests the magic number. @@ -1450,8 +1611,9 @@ public class BinaryDictInputOutput { inStream = new FileInputStream(file); final ByteBuffer buffer = inStream.getChannel().map( FileChannel.MapMode.READ_ONLY, 0, file.length()); - final int version = getFormatVersion(buffer); - return (version >= MINIMUM_SUPPORTED_VERSION && version <= MAXIMUM_SUPPORTED_VERSION); + final int version = getFormatVersion(new ByteBufferWrapper(buffer)); + return (version >= FormatSpec.MINIMUM_SUPPORTED_VERSION + && version <= FormatSpec.MAXIMUM_SUPPORTED_VERSION); } catch (FileNotFoundException e) { return false; } catch (IOException e) { @@ -1478,8 +1640,8 @@ public class BinaryDictInputOutput { */ public static int reconstructBigramFrequency(final int unigramFrequency, final int bigramFrequency) { - final float stepSize = (MAX_TERMINAL_FREQUENCY - unigramFrequency) - / (1.5f + MAX_BIGRAM_FREQUENCY); + final float stepSize = (FormatSpec.MAX_TERMINAL_FREQUENCY - unigramFrequency) + / (1.5f + FormatSpec.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/CharGroupInfo.java b/java/src/com/android/inputmethod/latin/makedict/CharGroupInfo.java index ef7dbb251..ed9388409 100644 --- a/java/src/com/android/inputmethod/latin/makedict/CharGroupInfo.java +++ b/java/src/com/android/inputmethod/latin/makedict/CharGroupInfo.java @@ -31,18 +31,20 @@ public class CharGroupInfo { public final int[] mCharacters; public final int mFrequency; public final int mChildrenAddress; + public final int mParentAddress; public final ArrayList<WeightedString> mShortcutTargets; public final ArrayList<PendingAttribute> mBigrams; public CharGroupInfo(final int originalAddress, final int endAddress, final int flags, - final int[] characters, final int frequency, final int childrenAddress, - final ArrayList<WeightedString> shortcutTargets, + final int[] characters, final int frequency, final int parentAddress, + final int childrenAddress, final ArrayList<WeightedString> shortcutTargets, final ArrayList<PendingAttribute> bigrams) { mOriginalAddress = originalAddress; mEndAddress = endAddress; mFlags = flags; mCharacters = characters; mFrequency = frequency; + mParentAddress = parentAddress; mChildrenAddress = childrenAddress; mShortcutTargets = shortcutTargets; mBigrams = bigrams; diff --git a/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java new file mode 100644 index 000000000..1707ccc39 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java @@ -0,0 +1,235 @@ +/* + * 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.makedict; + +import com.android.inputmethod.latin.Constants; +import com.android.inputmethod.latin.makedict.FusionDictionary.DictionaryOptions; + +/** + * Dictionary File Format Specification. + */ +public final class FormatSpec { + + /* + * Array of Node(FusionDictionary.Node) layout is as follows: + * + * g | + * r | the number of groups, 1 or 2 bytes. + * o | 1 byte = bbbbbbbb match + * u | case 1xxxxxxx => xxxxxxx << 8 + next byte + * p | otherwise => bbbbbbbb + * c | + * ount + * + * g | + * r | sequence of groups, + * o | the layout of each group is described below. + * u | + * ps + * + */ + + /* Node(CharGroup) layout is as follows: + * | addressType xx : mask with MASK_GROUP_ADDRESS_TYPE + * 2 bits, 00 = no children : FLAG_GROUP_ADDRESS_TYPE_NOADDRESS + * f | 01 = 1 byte : FLAG_GROUP_ADDRESS_TYPE_ONEBYTE + * l | 10 = 2 bytes : FLAG_GROUP_ADDRESS_TYPE_TWOBYTES + * a | 11 = 3 bytes : FLAG_GROUP_ADDRESS_TYPE_THREEBYTES + * g | has several chars ? 1 bit, 1 = yes, 0 = no : FLAG_HAS_MULTIPLE_CHARS + * 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 + * + * p | + * a | IF HAS_PARENT_ADDRESS (defined in the file header) + * r | parent address, 3byte + * e | the address must be negative, so the absolute value of the address is stored. + * n | + * taddress + * + * c | IF FLAG_HAS_MULTIPLE_CHARS + * h | char, char, char, char n * (1 or 3 bytes) : use CharGroupInfo for i/o helpers + * a | end 1 byte, = 0 + * r | ELSE + * s | char 1 or 3 bytes + * | END + * + * f | + * r | IF FLAG_IS_TERMINAL + * e | frequency 1 byte + * q | + * + * c | IF 00 = FLAG_GROUP_ADDRESS_TYPE_NOADDRESS = addressType + * h | // nothing + * i | ELSIF 01 = FLAG_GROUP_ADDRESS_TYPE_ONEBYTE == addressType + * l | children address, 1 byte + * d | ELSIF 10 = FLAG_GROUP_ADDRESS_TYPE_TWOBYTES == addressType + * r | children address, 2 bytes + * e | ELSE // 11 = FLAG_GROUP_ADDRESS_TYPE_THREEBYTES = addressType + * n | children address, 3 bytes + * A | END + * d + * dress + * + * | IF FLAG_IS_TERMINAL && FLAG_HAS_SHORTCUT_TARGETS + * | shortcut string list + * | IF FLAG_IS_TERMINAL && FLAG_HAS_BIGRAMS + * | bigrams address list + * + * Char format is: + * 1 byte = bbbbbbbb match + * case 000xxxxx: xxxxx << 16 + next byte << 8 + next byte + * else: if 00011111 (= 0x1F) : this is the terminator. This is a relevant choice because + * unicode code points range from 0 to 0x10FFFF, so any 3-byte value starting with + * 00011111 would be outside unicode. + * else: iso-latin-1 code + * This allows for the whole unicode range to be encoded, including chars outside of + * the BMP. Also everything in the iso-latin-1 charset is only 1 byte, except control + * characters which should never happen anyway (and still work, but take 3 bytes). + * + * bigram address list is: + * <flags> = | hasNext = 1 bit, 1 = yes, 0 = no : FLAG_ATTRIBUTE_HAS_NEXT + * | addressSign = 1 bit, : FLAG_ATTRIBUTE_OFFSET_NEGATIVE + * | 1 = must take -address, 0 = must take +address + * | xx : mask with MASK_ATTRIBUTE_ADDRESS_TYPE + * | addressFormat = 2 bits, 00 = unused : FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE + * | 01 = 1 byte : FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE + * | 10 = 2 bytes : FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES + * | 11 = 3 bytes : FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES + * | 4 bits : frequency : mask with FLAG_ATTRIBUTE_FREQUENCY + * <address> | IF (01 == FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE == addressFormat) + * | read 1 byte, add top 4 bits + * | ELSIF (10 == FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES == addressFormat) + * | read 2 bytes, add top 4 bits + * | ELSE // 11 == FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES == addressFormat + * | read 3 bytes, add top 4 bits + * | END + * | if (FLAG_ATTRIBUTE_OFFSET_NEGATIVE) then address = -address + * if (FLAG_ATTRIBUTE_HAS_NEXT) goto bigram_and_shortcut_address_list_is + * + * shortcut string list is: + * <byte size> = GROUP_SHORTCUT_LIST_SIZE_SIZE bytes, big-endian: size of the list, in bytes. + * <flags> = | hasNext = 1 bit, 1 = yes, 0 = no : FLAG_ATTRIBUTE_HAS_NEXT + * | reserved = 3 bits, must be 0 + * | 4 bits : frequency : mask with FLAG_ATTRIBUTE_FREQUENCY + * <shortcut> = | string of characters at the char format described above, with the terminator + * | used to signal the end of the string. + * if (FLAG_ATTRIBUTE_HAS_NEXT goto flags + */ + + static final int VERSION_1_MAGIC_NUMBER = 0x78B1; + public static final int VERSION_2_MAGIC_NUMBER = 0x9BC13AFE; + static final int MINIMUM_SUPPORTED_VERSION = 1; + static final int MAXIMUM_SUPPORTED_VERSION = 3; + static final int NOT_A_VERSION_NUMBER = -1; + static final int FIRST_VERSION_WITH_HEADER_SIZE = 2; + static final int FIRST_VERSION_WITH_PARENT_ADDRESS = 3; + + // These options need to be the same numeric values as the one in the native reading code. + static final int GERMAN_UMLAUT_PROCESSING_FLAG = 0x1; + static final int HAS_PARENT_ADDRESS = 0x2; + static final int FRENCH_LIGATURE_PROCESSING_FLAG = 0x4; + static final int CONTAINS_BIGRAMS_FLAG = 0x8; + + // TODO: Make this value adaptative to content data, store it in the header, and + // use it in the reading code. + static final int MAX_WORD_LENGTH = Constants.Dictionary.MAX_WORD_LENGTH; + + static final int PARENT_ADDRESS_SIZE = 3; + + static final int MASK_GROUP_ADDRESS_TYPE = 0xC0; + static final int FLAG_GROUP_ADDRESS_TYPE_NOADDRESS = 0x00; + static final int FLAG_GROUP_ADDRESS_TYPE_ONEBYTE = 0x40; + static final int FLAG_GROUP_ADDRESS_TYPE_TWOBYTES = 0x80; + static final int FLAG_GROUP_ADDRESS_TYPE_THREEBYTES = 0xC0; + + static final int FLAG_HAS_MULTIPLE_CHARS = 0x20; + + static final int FLAG_IS_TERMINAL = 0x10; + static final int FLAG_HAS_SHORTCUT_TARGETS = 0x08; + static final int FLAG_HAS_BIGRAMS = 0x04; + static final int FLAG_IS_NOT_A_WORD = 0x02; + static final int FLAG_IS_BLACKLISTED = 0x01; + + static final int FLAG_ATTRIBUTE_HAS_NEXT = 0x80; + static final int FLAG_ATTRIBUTE_OFFSET_NEGATIVE = 0x40; + static final int MASK_ATTRIBUTE_ADDRESS_TYPE = 0x30; + static final int FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE = 0x10; + static final int FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES = 0x20; + static final int FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES = 0x30; + static final int FLAG_ATTRIBUTE_FREQUENCY = 0x0F; + + static final int GROUP_CHARACTERS_TERMINATOR = 0x1F; + + static final int GROUP_TERMINATOR_SIZE = 1; + static final int GROUP_FLAGS_SIZE = 1; + static final int GROUP_FREQUENCY_SIZE = 1; + static final int GROUP_MAX_ADDRESS_SIZE = 3; + static final int GROUP_ATTRIBUTE_FLAGS_SIZE = 1; + static final int GROUP_ATTRIBUTE_MAX_ADDRESS_SIZE = 3; + static final int GROUP_SHORTCUT_LIST_SIZE_SIZE = 2; + + static final int NO_CHILDREN_ADDRESS = Integer.MIN_VALUE; + static final int NO_PARENT_ADDRESS = 0; + static final int INVALID_CHARACTER = -1; + + static final int MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT = 0x7F; // 127 + static final int MAX_CHARGROUPS_IN_A_NODE = 0x7FFF; // 32767 + + static final int MAX_TERMINAL_FREQUENCY = 255; + static final int MAX_BIGRAM_FREQUENCY = 15; + + /** + * Options about file format. + */ + public static class FormatOptions { + public final int mVersion; + public final boolean mHasParentAddress; + public FormatOptions(final int version) { + this(version, false); + } + public FormatOptions(final int version, final boolean hasParentAddress) { + mVersion = version; + if (version < FormatSpec.FIRST_VERSION_WITH_PARENT_ADDRESS && hasParentAddress) { + throw new RuntimeException("Parent addresses are only supported with versions " + + FormatSpec.FIRST_VERSION_WITH_PARENT_ADDRESS + " and ulterior."); + } + mHasParentAddress = hasParentAddress; + } + } + + /** + * Class representing file header. + */ + static final class FileHeader { + public final int mHeaderSize; + public final DictionaryOptions mDictionaryOptions; + public final FormatOptions mFormatOptions; + public FileHeader(final int headerSize, final DictionaryOptions dictionaryOptions, + final FormatOptions formatOptions) { + mHeaderSize = headerSize; + mDictionaryOptions = dictionaryOptions; + mFormatOptions = formatOptions; + } + } + + private FormatSpec() { + // This utility class is not publicly instantiable. + } +} diff --git a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java index 7c15ba54d..6775de8a8 100644 --- a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java +++ b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java @@ -16,6 +16,8 @@ package com.android.inputmethod.latin.makedict; +import com.android.inputmethod.latin.Constants; + import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -41,17 +43,15 @@ public class FusionDictionary implements Iterable<Word> { public static class Node { ArrayList<CharGroup> mData; // To help with binary generation - int mCachedSize; - int mCachedAddress; + int mCachedSize = Integer.MIN_VALUE; + int mCachedAddress = Integer.MIN_VALUE; + int mCachedParentAddress = 0; + public Node() { mData = new ArrayList<CharGroup>(); - mCachedSize = Integer.MIN_VALUE; - mCachedAddress = Integer.MIN_VALUE; } public Node(ArrayList<CharGroup> data) { mData = data; - mCachedSize = Integer.MIN_VALUE; - mCachedAddress = Integer.MIN_VALUE; } } @@ -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,10 +375,18 @@ 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); + if (word.length >= Constants.Dictionary.MAX_WORD_LENGTH) { + MakedictLog.w("Ignoring a word that is too long: word.length = " + word.length); + return; + } + Node currentNode = mRoot; int charIndex = 0; @@ -376,7 +410,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 +420,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 +436,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 +446,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 +526,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; } @@ -689,7 +733,7 @@ public class FusionDictionary implements Iterable<Word> { // StringBuilder s = new StringBuilder(); // for (CharGroup g : node.data) { // s.append(g.frequency); -// for (int ch : g.chars){ +// for (int ch : g.chars) { // s.append(Character.toChars(ch)); // } // } @@ -748,13 +792,14 @@ 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(); mCurrentString.setLength(mCurrentString.length() - mPositions.getLast().length); } - } while(true); + } while (true); } @Override 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/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 03263d274..9e8ab81b0 100644 --- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java +++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java @@ -61,6 +61,7 @@ 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; @@ -196,15 +197,15 @@ 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 = getFraction(a, + final float alphaValidTypedWord = ResourceUtils.getFraction(a, R.styleable.SuggestionStripView_alphaValidTypedWord, 1.0f); - final float alphaTypedWord = getFraction(a, + final float alphaTypedWord = ResourceUtils.getFraction(a, R.styleable.SuggestionStripView_alphaTypedWord, 1.0f); - final float alphaAutoCorrect = getFraction(a, + final float alphaAutoCorrect = ResourceUtils.getFraction(a, R.styleable.SuggestionStripView_alphaAutoCorrect, 1.0f); - final float alphaSuggested = getFraction(a, + final float alphaSuggested = ResourceUtils.getFraction(a, R.styleable.SuggestionStripView_alphaSuggested, 1.0f); - mAlphaObsoleted = getFraction(a, + mAlphaObsoleted = ResourceUtils.getFraction(a, R.styleable.SuggestionStripView_alphaSuggested, 1.0f); mColorValidTypedWord = applyAlpha(a.getColor( R.styleable.SuggestionStripView_colorValidTypedWord, 0), alphaValidTypedWord); @@ -217,13 +218,13 @@ public class SuggestionStripView extends RelativeLayout implements OnClickListen mSuggestionsCountInStrip = a.getInt( R.styleable.SuggestionStripView_suggestionsCountInStrip, DEFAULT_SUGGESTIONS_COUNT_IN_STRIP); - mCenterSuggestionWeight = getFraction(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 = getFraction(a, + mMinMoreSuggestionsWidth = ResourceUtils.getFraction(a, R.styleable.SuggestionStripView_minMoreSuggestionsWidth, 1.0f); a.recycle(); @@ -278,10 +279,6 @@ public class SuggestionStripView extends RelativeLayout implements OnClickListen return new BitmapDrawable(res, buffer); } - static float getFraction(final TypedArray a, final int index, final float defValue) { - return a.getFraction(index, 1, 1, defValue); - } - private CharSequence getStyledSuggestionWord(SuggestedWords suggestedWords, int pos) { final CharSequence word = suggestedWords.getWord(pos); final boolean isAutoCorrect = pos == 1 && suggestedWords.willAutoCorrect(); diff --git a/java/src/com/android/inputmethod/research/ResearchLog.java b/java/src/com/android/inputmethod/research/ResearchLog.java index 71a6d6a78..70c38e909 100644 --- a/java/src/com/android/inputmethod/research/ResearchLog.java +++ b/java/src/com/android/inputmethod/research/ResearchLog.java @@ -93,7 +93,7 @@ public class ResearchLog { mFile = outputFile; } - public synchronized void close() { + public synchronized void close(final Runnable onClosed) { mExecutor.submit(new Callable<Object>() { @Override public Object call() throws Exception { @@ -102,7 +102,14 @@ public class ResearchLog { mJsonWriter.endArray(); mJsonWriter.flush(); mJsonWriter.close(); + if (DEBUG) { + Log.d(TAG, "wrote log to " + mFile); + } mHasWrittenData = false; + } else { + if (DEBUG) { + Log.d(TAG, "close() called, but no data, not outputting"); + } } } catch (Exception e) { Log.d(TAG, "error when closing ResearchLog:"); @@ -111,6 +118,9 @@ public class ResearchLog { if (mFile.exists()) { mFile.setWritable(false, false); } + if (onClosed != null) { + onClosed.run(); + } } return null; } @@ -257,7 +267,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); diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java index 918fcf5a1..763fd6e00 100644 --- a/java/src/com/android/inputmethod/research/ResearchLogger.java +++ b/java/src/com/android/inputmethod/research/ResearchLogger.java @@ -35,6 +35,7 @@ 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; @@ -43,15 +44,12 @@ 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; @@ -86,6 +84,7 @@ import java.util.UUID; */ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChangeListener { private static final String TAG = ResearchLogger.class.getSimpleName(); + private static final boolean DEBUG = false; private static final boolean OUTPUT_ENTIRE_BUFFER = false; // true may disclose private info public static final boolean DEFAULT_USABILITY_STUDY_MODE = false; /* package */ static boolean sIsLogging = false; @@ -251,44 +250,49 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang 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; } @@ -341,6 +345,9 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } private void start() { + if (DEBUG) { + Log.d(TAG, "start called"); + } maybeShowSplashScreen(); updateSuspendedState(); requestIndicatorRedraw(); @@ -368,21 +375,27 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } /* package */ void stop() { + if (DEBUG) { + Log.d(TAG, "stop called"); + } logStatistics(); commitCurrentLogUnit(); if (mMainLogBuffer != null) { publishLogBuffer(mMainLogBuffer, mMainResearchLog, false /* isIncludingPrivateData */); - mMainResearchLog.close(); + mMainResearchLog.close(null /* callback */); mMainLogBuffer = null; } if (mFeedbackLogBuffer != null) { - mFeedbackLog.close(); + mFeedbackLog.close(null /* callback */); mFeedbackLogBuffer = null; } } public boolean abort() { + if (DEBUG) { + Log.d(TAG, "abort called"); + } boolean didAbortMainLog = false; if (mMainLogBuffer != null) { mMainLogBuffer.clear(); @@ -450,12 +463,18 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang 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[] { @@ -472,28 +491,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; } } @@ -504,6 +502,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang .setTitle(title); latinIME.showOptionDialog(builder.create()); } + */ private boolean mInFeedbackDialog = false; public void presentFeedbackDialog(LatinIME latinIME) { @@ -511,6 +510,35 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang latinIME.launchKeyboardedDialogActivity(FeedbackActivity.class); } + // 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 { + 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(); + } + } + */ + private static final String[] EVENTKEYS_FEEDBACK = { "UserTimestamp", "contents" }; @@ -531,12 +559,19 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang false /* isPotentiallyPrivate */); mFeedbackLogBuffer.shiftIn(feedbackLogUnit); publishLogBuffer(mFeedbackLogBuffer, mFeedbackLog, true /* isIncludingPrivateData */); - mFeedbackLog.close(); - uploadNow(); + mFeedbackLog.close(new Runnable() { + @Override + public void run() { + uploadNow(); + } + }); mFeedbackLog = new ResearchLog(createLogFile(mFilesDir)); } public void uploadNow() { + if (DEBUG) { + Log.d(TAG, "calling uploadNow()"); + } mInputMethodService.startService(mUploadIntent); } @@ -556,6 +591,13 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } private boolean isAllowedToLog() { + if (DEBUG) { + Log.d(TAG, "iatl: " + + "mipw=" + mIsPasswordView + + ", mils=" + mIsLoggingSuspended + + ", sil=" + sIsLogging + + ", mInFeedbackDialog=" + mInFeedbackDialog); + } return !mIsPasswordView && !mIsLoggingSuspended && sIsLogging && !mInFeedbackDialog; } @@ -644,6 +686,9 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } /* package for test */ void commitCurrentLogUnit() { + if (DEBUG) { + Log.d(TAG, "commitCurrentLogUnit"); + } if (!mCurrentLogUnit.isEmpty()) { if (mMainLogBuffer != null) { mMainLogBuffer.shiftIn(mCurrentLogUnit); @@ -996,26 +1041,26 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang final boolean isPasswordView = kid.passwordInput(); getInstance().setIsPasswordView(isPasswordView); final Object[] values = { - KeyboardId.elementIdToName(kid.mElementId), - kid.mLocale + ":" + kid.mSubtype.getExtraValueOf(KEYBOARD_LAYOUT_SET), - kid.mOrientation, - kid.mWidth, - KeyboardId.modeName(kid.mMode), - kid.imeAction(), - kid.navigateNext(), - kid.navigatePrevious(), - kid.mClobberSettingsKey, - isPasswordView, - kid.mShortcutKeyEnabled, - kid.mHasShortcutKey, - kid.mLanguageSwitchKeyEnabled, - kid.isMultiLine(), - keyboard.mOccupiedWidth, - keyboard.mOccupiedHeight, - keyboard.mKeys - }; - getInstance().enqueueEvent(EVENTKEYS_MAINKEYBOARDVIEW_SETKEYBOARD, values); + KeyboardId.elementIdToName(kid.mElementId), + kid.mLocale + ":" + kid.mSubtype.getExtraValueOf(KEYBOARD_LAYOUT_SET), + kid.mOrientation, + kid.mWidth, + KeyboardId.modeName(kid.mMode), + kid.imeAction(), + kid.navigateNext(), + kid.navigatePrevious(), + kid.mClobberSettingsKey, + isPasswordView, + kid.mShortcutKeyEnabled, + kid.mHasShortcutKey, + kid.mLanguageSwitchKeyEnabled, + kid.isMultiLine(), + keyboard.mOccupiedWidth, + keyboard.mOccupiedHeight, + keyboard.mKeys + }; getInstance().setIsPasswordView(isPasswordView); + getInstance().enqueueEvent(EVENTKEYS_MAINKEYBOARDVIEW_SETKEYBOARD, values); } } @@ -1045,7 +1090,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()), |