diff options
48 files changed, 955 insertions, 490 deletions
diff --git a/java/res/values/dictionary-pack.xml b/java/res/values/dictionary-pack.xml index 4109bcb95..3fdc67132 100644 --- a/java/res/values/dictionary-pack.xml +++ b/java/res/values/dictionary-pack.xml @@ -20,4 +20,8 @@ <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="dictionary_pack_client_id" translatable="false">com.android.inputmethod.latin</string> <string name="dictionary_pack_metadata_uri" translatable="false"></string> + <string name="dictionary_pack_settings_activity">com.android.inputmethod.dictionarypack.DictionarySettingsActivity</string> + <string name="authority">com.android.inputmethod.dictionarypack.aosp</string> + <string name="default_metadata_uri"></string> + <string name="local_metadata_filename">metadata.json</string> </resources> diff --git a/java/res/values/donottranslate.xml b/java/res/values/donottranslate.xml index 1e70fbbba..d2554ee5f 100644 --- a/java/res/values/donottranslate.xml +++ b/java/res/values/donottranslate.xml @@ -211,11 +211,4 @@ </string-array> <string name="settings_warning_researcher_mode">Attention! You are using the special keyboard for research purposes.</string> - - <!-- dictionary pack settings --> - <string name="dictionary_pack_settings_activity">com.android.inputmethod.dictionarypack.DictionarySettingsActivity</string> - <string name="authority">com.android.inputmethod.dictionarypack.aosp</string> - <string name="default_metadata_uri"></string> - <string name="local_metadata_filename">metadata.json</string> - </resources> diff --git a/java/res/values/keypress-vibration-durations.xml b/java/res/values/keypress-vibration-durations.xml index 9b1d5431e..10400be83 100644 --- a/java/res/values/keypress-vibration-durations.xml +++ b/java/res/values/keypress-vibration-durations.xml @@ -18,13 +18,17 @@ */ --> <resources> + <!-- Build.HARDWARE,duration_in_milliseconds --> <string-array name="keypress_vibration_durations" translatable="false"> - <!-- Build.HARDWARE,duration_in_milliseconds --> + <!-- Nexus S --> <item>herring,5</item> + <!-- Galaxy Nexus --> <item>tuna,5</item> + <!-- Nexus 4 --> <item>mako,5</item> + <!-- Nexus 10 --> <item>manta,16</item> <!-- Default value for unknown device --> - <item>DEFAULT,10</item> + <item>DEFAULT,20</item> </string-array> </resources> diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml index adbf52953..201fc7030 100644 --- a/java/res/values/strings.xml +++ b/java/res/values/strings.xml @@ -498,9 +498,9 @@ <!-- Message about some dictionary indicating the file is installed, but the dictionary is disabled --> <string name="dictionary_disabled">Installed, disabled</string> - <!-- Message to display in the dictionaries setting screen when some error prevented us to list installed dictionaries [CHAR LIMIT=50] --> + <!-- Message to display in the dictionaries setting screen when some error prevented us to list installed dictionaries [CHAR LIMIT=20] --> <string name="cannot_connect_to_dict_service">Problem connecting to dictionary service</string> - <!-- Message to display in the dictionaries setting screen when we found that no dictionaries are available [CHAR LIMIT=50]--> + <!-- Message to display in the dictionaries setting screen when we found that no dictionaries are available [CHAR LIMIT=20]--> <string name="no_dictionaries_available">No dictionaries available</string> <!-- Title of the options to press to refresh the list (as in, check for updates now) [CHAR_LIMIT=50] --> diff --git a/java/res/xml/prefs.xml b/java/res/xml/prefs.xml index 783946252..a13021bd0 100644 --- a/java/res/xml/prefs.xml +++ b/java/res/xml/prefs.xml @@ -90,6 +90,7 @@ android:summary="@string/gesture_input_summary" android:persistent="true" android:defaultValue="true" /> + <!-- TODO: Move these two options to the advanced settings. --> <CheckBoxPreference android:key="pref_gesture_floating_preview_text" android:title="@string/gesture_floating_preview_text" @@ -139,10 +140,6 @@ android:summary="@string/include_other_imes_in_language_switch_list_summary" android:persistent="true" android:defaultValue="false" /> - <PreferenceScreen - android:fragment="com.android.inputmethod.latin.AdditionalSubtypeSettings" - android:key="custom_input_styles" - android:title="@string/custom_input_styles_title" /> <!-- Values for popup dismiss delay are added programmatically --> <CheckBoxPreference android:key="pref_sliding_key_input_preview" @@ -150,6 +147,10 @@ android:summary="@string/sliding_key_input_preview_summary" android:persistent="true" android:defaultValue="true" /> + <PreferenceScreen + android:fragment="com.android.inputmethod.latin.AdditionalSubtypeSettings" + android:key="custom_input_styles" + android:title="@string/custom_input_styles_title" /> <ListPreference android:key="pref_key_preview_popup_dismiss_delay" android:title="@string/key_preview_popup_dismiss_delay" /> diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardView.java b/java/src/com/android/inputmethod/keyboard/KeyboardView.java index 43d28be5d..e4e75c342 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyboardView.java +++ b/java/src/com/android/inputmethod/keyboard/KeyboardView.java @@ -26,10 +26,8 @@ import android.graphics.Paint.Align; import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.Region; -import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.util.AttributeSet; -import android.util.SparseArray; import android.view.View; import com.android.inputmethod.keyboard.internal.KeyDrawParams; @@ -73,15 +71,15 @@ import java.util.HashSet; */ public class KeyboardView extends View { // XML attributes - protected final KeyVisualAttributes mKeyVisualAttributes; + 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 Drawable mKeyBackground; - protected final Rect mKeyBackgroundPadding = new Rect(); + private final float mVerticalCorrection; + private final Drawable mKeyBackground; + private final Rect mKeyBackgroundPadding = new Rect(); // HORIZONTAL ELLIPSIS "...", character for popup hint. private static final String POPUP_HINT_CHAR = "\u2026"; @@ -113,10 +111,6 @@ public class KeyboardView extends View { 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. - private static final SparseArray<Float> sTextHeightCache = CollectionUtils.newSparseArray(); - // This sparse array caches key label text width in pixel indexed by key label text size. - private static final SparseArray<Float> sTextWidthCache = CollectionUtils.newSparseArray(); private static final char[] KEY_LABEL_REFERENCE_CHAR = { 'M' }; private static final char[] KEY_NUMERIC_HINT_LABEL_REFERENCE_CHAR = { '8' }; @@ -134,15 +128,15 @@ public class KeyboardView extends View { mKeyLabelHorizontalPadding = keyboardViewAttr.getDimensionPixelOffset( R.styleable.KeyboardView_keyLabelHorizontalPadding, 0); mKeyHintLetterPadding = keyboardViewAttr.getDimension( - R.styleable.KeyboardView_keyHintLetterPadding, 0); + R.styleable.KeyboardView_keyHintLetterPadding, 0.0f); mKeyPopupHintLetterPadding = keyboardViewAttr.getDimension( - R.styleable.KeyboardView_keyPopupHintLetterPadding, 0); + R.styleable.KeyboardView_keyPopupHintLetterPadding, 0.0f); mKeyShiftedLetterHintPadding = keyboardViewAttr.getDimension( - R.styleable.KeyboardView_keyShiftedLetterHintPadding, 0); + R.styleable.KeyboardView_keyShiftedLetterHintPadding, 0.0f); mKeyTextShadowRadius = keyboardViewAttr.getFloat( R.styleable.KeyboardView_keyTextShadowRadius, 0.0f); mVerticalCorrection = keyboardViewAttr.getDimension( - R.styleable.KeyboardView_verticalCorrection, 0); + R.styleable.KeyboardView_verticalCorrection, 0.0f); keyboardViewAttr.recycle(); final TypedArray keyAttr = context.obtainStyledAttributes(attrs, @@ -185,6 +179,14 @@ public class KeyboardView extends View { return mKeyboard; } + protected float getVerticalCorrection() { + return mVerticalCorrection; + } + + protected void updateKeyDrawParams(final int keyHeight) { + mKeyDrawParams.updateParams(keyHeight, mKeyVisualAttributes); + } + @Override protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { if (mKeyboard != null) { @@ -213,7 +215,7 @@ public class KeyboardView extends View { } onDrawKeyboard(mOffscreenCanvas); } - canvas.drawBitmap(mOffscreenBuffer, 0, 0, null); + canvas.drawBitmap(mOffscreenBuffer, 0.0f, 0.0f, null); } private boolean maybeAllocateOffscreenBuffer() { @@ -333,7 +335,7 @@ public class KeyboardView extends View { canvas.translate(bgX, bgY); background.draw(canvas); if (LatinImeLogger.sVISUALDEBUG) { - drawRectangle(canvas, 0, 0, bgWidth, bgHeight, 0x80c00000, new Paint()); + drawRectangle(canvas, 0.0f, 0.0f, bgWidth, bgHeight, 0x80c00000, new Paint()); } canvas.translate(-bgX, -bgY); } @@ -347,7 +349,7 @@ public class KeyboardView extends View { final float centerY = keyHeight * 0.5f; if (LatinImeLogger.sVISUALDEBUG) { - drawRectangle(canvas, 0, 0, keyWidth, keyHeight, 0x800000c0, new Paint()); + drawRectangle(canvas, 0.0f, 0.0f, keyWidth, keyHeight, 0x800000c0, new Paint()); } // Draw key label. @@ -357,14 +359,16 @@ public class KeyboardView extends View { final String label = key.mLabel; 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); + final float labelCharHeight = TypefaceUtils.getCharHeight( + KEY_LABEL_REFERENCE_CHAR, paint); + final float labelCharWidth = TypefaceUtils.getCharWidth( + KEY_LABEL_REFERENCE_CHAR, paint); // Vertical label text alignment. - final float baseline = centerY + labelCharHeight / 2; + final float baseline = centerY + labelCharHeight / 2.0f; // Horizontal label text alignment - float labelWidth = 0; + float labelWidth = 0.0f; if (key.isAlignLeft()) { positionX = mKeyLabelHorizontalPadding; paint.setTextAlign(Align.LEFT); @@ -373,31 +377,31 @@ public class KeyboardView extends View { paint.setTextAlign(Align.RIGHT); } else if (key.isAlignLeftOfCenter()) { // TODO: Parameterise this? - positionX = centerX - labelCharWidth * 7 / 4; + positionX = centerX - labelCharWidth * 7.0f / 4.0f; paint.setTextAlign(Align.LEFT); } else if (key.hasLabelWithIconLeft() && icon != null) { - labelWidth = getLabelWidth(label, paint) + icon.getIntrinsicWidth() + labelWidth = TypefaceUtils.getLabelWidth(label, paint) + icon.getIntrinsicWidth() + LABEL_ICON_MARGIN * keyWidth; - positionX = centerX + labelWidth / 2; + positionX = centerX + labelWidth / 2.0f; paint.setTextAlign(Align.RIGHT); } else if (key.hasLabelWithIconRight() && icon != null) { - labelWidth = getLabelWidth(label, paint) + icon.getIntrinsicWidth() + labelWidth = TypefaceUtils.getLabelWidth(label, paint) + icon.getIntrinsicWidth() + LABEL_ICON_MARGIN * keyWidth; - positionX = centerX - labelWidth / 2; + positionX = centerX - labelWidth / 2.0f; paint.setTextAlign(Align.LEFT); } else { positionX = centerX; paint.setTextAlign(Align.CENTER); } if (key.needsXScale()) { - paint.setTextScaleX( - Math.min(1.0f, (keyWidth * MAX_LABEL_RATIO) / getLabelWidth(label, paint))); + paint.setTextScaleX(Math.min(1.0f, + (keyWidth * MAX_LABEL_RATIO) / TypefaceUtils.getLabelWidth(label, paint))); } paint.setColor(key.selectTextColor(params)); if (key.isEnabled()) { // Set a drop shadow for the text - paint.setShadowLayer(mKeyTextShadowRadius, 0, 0, params.mTextShadowColor); + paint.setShadowLayer(mKeyTextShadowRadius, 0.0f, 0.0f, params.mTextShadowColor); } else { // Make label invisible paint.setColor(Color.TRANSPARENT); @@ -405,7 +409,7 @@ public class KeyboardView extends View { 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); + paint.setShadowLayer(0.0f, 0.0f, 0.0f, Color.TRANSPARENT); paint.setTextScaleX(1.0f); if (icon != null) { @@ -413,10 +417,10 @@ public class KeyboardView extends View { final int iconHeight = icon.getIntrinsicHeight(); final int iconY = (keyHeight - iconHeight) / 2; if (key.hasLabelWithIconLeft()) { - final int iconX = (int)(centerX - labelWidth / 2); + final int iconX = (int)(centerX - labelWidth / 2.0f); drawIcon(canvas, icon, iconX, iconY, iconWidth, iconHeight); } else if (key.hasLabelWithIconRight()) { - final int iconX = (int)(centerX + labelWidth / 2 - iconWidth); + final int iconX = (int)(centerX + labelWidth / 2.0f - iconWidth); drawIcon(canvas, icon, iconX, iconY, iconWidth, iconHeight); } } @@ -439,20 +443,23 @@ public class KeyboardView extends View { // The hint label is placed just right of the key label. Used mainly on // "phone number" layout. // TODO: Generalize the following calculations. - hintX = positionX + getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) * 2; - hintY = centerY + getCharHeight(KEY_LABEL_REFERENCE_CHAR, paint) / 2; + hintX = positionX + + TypefaceUtils.getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) * 2.0f; + hintY = centerY + + TypefaceUtils.getCharHeight(KEY_LABEL_REFERENCE_CHAR, paint) / 2.0f; 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 - mKeyShiftedLetterHintPadding - - getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) / 2; + - TypefaceUtils.getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) / 2.0f; 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 - mKeyHintLetterPadding - - getCharWidth(KEY_NUMERIC_HINT_LABEL_REFERENCE_CHAR, paint) / 2; + - TypefaceUtils.getCharWidth(KEY_NUMERIC_HINT_LABEL_REFERENCE_CHAR, paint) + / 2.0f; hintY = -paint.ascent(); paint.setTextAlign(Align.CENTER); } @@ -506,7 +513,7 @@ public class KeyboardView extends View { paint.setColor(params.mHintLabelColor); paint.setTextAlign(Align.CENTER); final float hintX = keyWidth - mKeyHintLetterPadding - - getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) / 2; + - TypefaceUtils.getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) / 2.0f; final float hintY = keyHeight - mKeyPopupHintLetterPadding; canvas.drawText(POPUP_HINT_CHAR, hintX, hintY, paint); @@ -517,54 +524,6 @@ public class KeyboardView extends View { } } - 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; - if (face == Typeface.DEFAULT) { - return codePointOffset + labelSize; - } else if (face == Typeface.DEFAULT_BOLD) { - return codePointOffset + labelSize + 0x1000; - } else if (face == Typeface.MONOSPACE) { - return codePointOffset + labelSize + 0x2000; - } else { - return codePointOffset + labelSize; - } - } - - // Working variable for the following methods. - private final Rect mTextBounds = new Rect(); - - 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) - return cachedValue; - - paint.getTextBounds(referenceChar, 0, 1, mTextBounds); - final float height = mTextBounds.height(); - sTextHeightCache.put(key, height); - return height; - } - - 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) - return cachedValue; - - paint.getTextBounds(referenceChar, 0, 1, mTextBounds); - final float width = mTextBounds.width(); - sTextWidthCache.put(key, width); - return width; - } - - // TODO: Remove this method. - public float getLabelWidth(final String label, final Paint paint) { - paint.getTextBounds(label, 0, label.length(), mTextBounds); - return mTextBounds.width(); - } - 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); @@ -578,7 +537,7 @@ public class KeyboardView extends View { paint.setStyle(Paint.Style.STROKE); paint.setStrokeWidth(1.0f); paint.setColor(color); - canvas.drawLine(0, y, w, y, paint); + canvas.drawLine(0.0f, y, w, y, paint); } private static void drawVerticalLine(final Canvas canvas, final float x, final float h, @@ -586,7 +545,7 @@ public class KeyboardView extends View { paint.setStyle(Paint.Style.STROKE); paint.setStrokeWidth(1.0f); paint.setColor(color); - canvas.drawLine(x, 0, x, h, paint); + canvas.drawLine(x, 0.0f, x, h, paint); } private static void drawRectangle(final Canvas canvas, final float x, final float y, @@ -595,15 +554,20 @@ public class KeyboardView extends View { paint.setStrokeWidth(1.0f); paint.setColor(color); canvas.translate(x, y); - canvas.drawRect(0, 0, w, h, paint); + canvas.drawRect(0.0f, 0.0f, w, h, paint); canvas.translate(-x, -y); } - public Paint newDefaultLabelPaint() { + public Paint newLabelPaint(final Key key) { final Paint paint = new Paint(); paint.setAntiAlias(true); - paint.setTypeface(mKeyDrawParams.mTypeface); - paint.setTextSize(mKeyDrawParams.mLabelSize); + if (key == null) { + paint.setTypeface(mKeyDrawParams.mTypeface); + paint.setTextSize(mKeyDrawParams.mLabelSize); + } else { + paint.setTypeface(key.selectTypeface(mKeyDrawParams)); + paint.setTextSize(key.selectTextSize(mKeyDrawParams)); + } return paint; } diff --git a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java index 745e7dfed..ba78d014a 100644 --- a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java +++ b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java @@ -530,9 +530,9 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack R.styleable.MainKeyboardView_altCodeKeyWhileTypingFadeinAnimator, 0); final float keyHysteresisDistance = mainKeyboardViewAttr.getDimension( - R.styleable.MainKeyboardView_keyHysteresisDistance, 0); + R.styleable.MainKeyboardView_keyHysteresisDistance, 0.0f); final float keyHysteresisDistanceForSlidingModifier = mainKeyboardViewAttr.getDimension( - R.styleable.MainKeyboardView_keyHysteresisDistanceForSlidingModifier, 0); + R.styleable.MainKeyboardView_keyHysteresisDistanceForSlidingModifier, 0.0f); mKeyDetector = new KeyDetector( keyHysteresisDistance, keyHysteresisDistanceForSlidingModifier); mKeyTimerHandler = new KeyTimerHandler(this, mainKeyboardViewAttr); @@ -655,7 +655,7 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack mKeyTimerHandler.cancelLongPressTimer(); super.setKeyboard(keyboard); mKeyDetector.setKeyboard( - keyboard, -getPaddingLeft(), -getPaddingTop() + mVerticalCorrection); + keyboard, -getPaddingLeft(), -getPaddingTop() + getVerticalCorrection()); PointerTracker.setKeyDetector(mKeyDetector); mTouchScreenRegulator.setKeyboardGeometry(keyboard.mOccupiedWidth); mMoreKeysKeyboardCache.clear(); @@ -1329,7 +1329,7 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack // Overlay a dark rectangle to dim. if (mNeedsToDimEntireKeyboard) { - canvas.drawRect(0, 0, getWidth(), getHeight(), mBackgroundDimAlphaPaint); + canvas.drawRect(0.0f, 0.0f, getWidth(), getHeight(), mBackgroundDimAlphaPaint); } } @@ -1353,9 +1353,10 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack } } - private boolean fitsTextIntoWidth(final int width, final String text, final Paint paint) { + private static boolean fitsTextIntoWidth(final int width, final String text, + final Paint paint) { paint.setTextScaleX(1.0f); - final float textWidth = getLabelWidth(text, paint); + final float textWidth = TypefaceUtils.getLabelWidth(text, paint); if (textWidth < width) { return true; } @@ -1366,12 +1367,12 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack } paint.setTextScaleX(scaleX); - return getLabelWidth(text, paint) < width; + return TypefaceUtils.getLabelWidth(text, paint) < width; } // Layout language name on spacebar. - private String layoutLanguageOnSpacebar(final Paint paint, final InputMethodSubtype subtype, - final int width) { + private static String layoutLanguageOnSpacebar(final Paint paint, + final InputMethodSubtype subtype, final int width) { // Choose appropriate language name to fit into the width. final String fullText = getFullDisplayName(subtype); if (fitsTextIntoWidth(width, fullText, paint)) { @@ -1460,7 +1461,7 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack return ""; } final Locale locale = SubtypeLocale.getSubtypeLocale(subtype); - return StringUtils.toTitleCase(locale.getLanguage(), locale); + return StringUtils.capitalizeFirstCodePoint(locale.getLanguage(), locale); } // Get InputMethodSubtype's middle display name in its locale. diff --git a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java index 66c30149c..ae08a5953 100644 --- a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java +++ b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java @@ -17,6 +17,7 @@ package com.android.inputmethod.keyboard; import android.content.Context; +import android.content.res.Resources; import android.graphics.Paint; import android.graphics.drawable.Drawable; @@ -73,10 +74,11 @@ public final class MoreKeysKeyboard extends Keyboard { final int rowHeight, final int coordXInParent, final int parentKeyboardWidth, final boolean isFixedColumnOrder, final int dividerWidth) { mIsFixedOrder = isFixedColumnOrder; - if (parentKeyboardWidth / keyWidth < maxColumns) { + if (parentKeyboardWidth / keyWidth < Math.min(numKeys, maxColumns)) { throw new IllegalArgumentException( "Keyboard is too small to hold more keys keyboard: " - + parentKeyboardWidth + " " + keyWidth + " " + maxColumns); + + parentKeyboardWidth + " " + keyWidth + " " + + numKeys + " " + maxColumns); } mDefaultKeyWidth = keyWidth; mDefaultRowHeight = rowHeight; @@ -257,7 +259,6 @@ public final class MoreKeysKeyboard extends Keyboard { private static final float LABEL_PADDING_RATIO = 0.2f; private static final float DIVIDER_RATIO = 0.2f; - /** * The builder of MoreKeysKeyboard. * @param context the context of {@link MoreKeysKeyboardView}. @@ -289,11 +290,23 @@ public final class MoreKeysKeyboard extends Keyboard { // be considered because the vertical positions of both backgrounds were already // adjusted with their bottom paddings deducted. width = keyPreviewDrawParams.mPreviewVisibleWidth; - height = keyPreviewDrawParams.mPreviewVisibleHeight - + mParams.mVerticalGap; + height = keyPreviewDrawParams.mPreviewVisibleHeight + mParams.mVerticalGap; + // TODO: Remove this check. + if (width == 0) { + throw new IllegalArgumentException( + "Zero width key detected: " + parentKey + " in " + parentKeyboard.mId); + } } else { - width = getMaxKeyWidth(parentKeyboardView, parentKey, mParams.mDefaultKeyWidth); + width = getMaxKeyWidth(parentKeyboardView, parentKey, mParams.mDefaultKeyWidth, + context.getResources()); height = parentKeyboard.mMostCommonKeyHeight; + // TODO: Remove this check. + if (width == 0) { + throw new IllegalArgumentException( + "Zero width calculated: " + parentKey + + " moreKeys=" + java.util.Arrays.toString(parentKey.mMoreKeys) + + " in " + parentKeyboard.mId); + } } final int dividerWidth; if (parentKey.needsDividersInMoreKeys()) { @@ -310,22 +323,18 @@ public final class MoreKeysKeyboard extends Keyboard { } 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.setTypeface(parentKey.selectTypeface(view.mKeyDrawParams)); - paint.setTextSize(parentKey.selectMoreKeyTextSize(view.mKeyDrawParams)); + final int minKeyWidth, final Resources res) { + final float padding = + res.getDimension(R.dimen.more_keys_keyboard_key_horizontal_padding) + + (parentKey.hasLabelsInMoreKeys() ? minKeyWidth * LABEL_PADDING_RATIO : 0.0f); + final Paint paint = view.newLabelPaint(parentKey); int maxWidth = minKeyWidth; for (final MoreKeySpec spec : parentKey.mMoreKeys) { final String label = spec.mLabel; // If the label is single letter, minKeyWidth is enough to hold the label. if (label != null && StringUtils.codePointCount(label) > 1) { - final int width = (int)view.getLabelWidth(label, paint) + padding; - if (maxWidth < width) { - maxWidth = width; - } + maxWidth = Math.max(maxWidth, + (int)(TypefaceUtils.getLabelWidth(label, paint) + padding)); } } return maxWidth; diff --git a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java index 0d42ab2fe..a356eb119 100644 --- a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java +++ b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java @@ -71,7 +71,7 @@ public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel public void setKeyboard(final Keyboard keyboard) { super.setKeyboard(keyboard); mKeyDetector.setKeyboard(keyboard, -getPaddingLeft(), - -getPaddingTop() + mVerticalCorrection); + -getPaddingTop() + getVerticalCorrection()); } @Override diff --git a/java/src/com/android/inputmethod/keyboard/ProximityInfo.java b/java/src/com/android/inputmethod/keyboard/ProximityInfo.java index 32cee734a..b77e378bf 100644 --- a/java/src/com/android/inputmethod/keyboard/ProximityInfo.java +++ b/java/src/com/android/inputmethod/keyboard/ProximityInfo.java @@ -26,7 +26,7 @@ import com.android.inputmethod.latin.JniUtils; import java.util.Arrays; -public final class ProximityInfo { +public class ProximityInfo { private static final String TAG = ProximityInfo.class.getSimpleName(); private static final boolean DEBUG = false; @@ -79,22 +79,21 @@ public final class ProximityInfo { mNativeProximityInfo = createNativeProximityInfo(touchPositionCorrection); } - private static ProximityInfo createDummyProximityInfo() { - return new ProximityInfo("", 1, 1, 1, 1, 1, 1, EMPTY_KEY_ARRAY, null); - } - - public static ProximityInfo createSpellCheckerProximityInfo(final int[] proximityCharsArray, - final int rowSize, final int gridWidth, final int gridHeight) { - final ProximityInfo spellCheckerProximityInfo = createDummyProximityInfo(); - spellCheckerProximityInfo.mNativeProximityInfo = - spellCheckerProximityInfo.setProximityInfoNative("" /* locale */, - gridWidth /* displayWidth */, gridHeight /* displayHeight */, - gridWidth, gridHeight, 1 /* mostCommonKeyWidth */, proximityCharsArray, - 0 /* keyCount */, null /*keyXCoordinates */, null /* keyYCoordinates */, - null /* keyWidths */, null /* keyHeights */, null /* keyCharCodes */, - null /* sweetSpotCenterXs */, null /* sweetSpotCenterYs */, - null /* sweetSpotRadii */); - return spellCheckerProximityInfo; + /** + * Constructor for subclasses such as + * {@link com.android.inputmethod.latin.spellcheck.SpellCheckerProximityInfo}. + */ + protected ProximityInfo(final int[] proximityCharsArray, final int gridWidth, + final int gridHeight) { + this("", 1, 1, 1, 1, 1, 1, EMPTY_KEY_ARRAY, null); + mNativeProximityInfo = setProximityInfoNative("" /* locale */, + gridWidth /* displayWidth */, gridHeight /* displayHeight */, + gridWidth, gridHeight, 1 /* mostCommonKeyWidth */, + 1 /* mostCommonKeyHeight */, proximityCharsArray, 0 /* keyCount */, + null /*keyXCoordinates */, null /* keyYCoordinates */, + null /* keyWidths */, null /* keyHeights */, null /* keyCharCodes */, + null /* sweetSpotCenterXs */, null /* sweetSpotCenterYs */, + null /* sweetSpotRadii */); } private long mNativeProximityInfo; @@ -105,9 +104,10 @@ public final class ProximityInfo { // TODO: Stop passing proximityCharsArray private static native long setProximityInfoNative(String locale, int displayWidth, int displayHeight, int gridWidth, int gridHeight, - int mostCommonKeyWidth, int[] proximityCharsArray, int keyCount, int[] keyXCoordinates, - int[] keyYCoordinates, int[] keyWidths, int[] keyHeights, int[] keyCharCodes, - float[] sweetSpotCenterXs, float[] sweetSpotCenterYs, float[] sweetSpotRadii); + int mostCommonKeyWidth, int mostCommonKeyHeight, int[] proximityCharsArray, + int keyCount, int[] keyXCoordinates, int[] keyYCoordinates, int[] keyWidths, + int[] keyHeights, int[] keyCharCodes, float[] sweetSpotCenterXs, + float[] sweetSpotCenterYs, float[] sweetSpotRadii); private static native void releaseProximityInfoNative(long nativeProximityInfo); @@ -234,9 +234,9 @@ public final class ProximityInfo { // TODO: Stop passing proximityCharsArray return setProximityInfoNative(mLocaleStr, mKeyboardMinWidth, mKeyboardHeight, - mGridWidth, mGridHeight, mMostCommonKeyWidth, proximityCharsArray, keyCount, - keyXCoordinates, keyYCoordinates, keyWidths, keyHeights, keyCharCodes, - sweetSpotCenterXs, sweetSpotCenterYs, sweetSpotRadii); + mGridWidth, mGridHeight, mMostCommonKeyWidth, mMostCommonKeyHeight, + proximityCharsArray, keyCount, keyXCoordinates, keyYCoordinates, keyWidths, + keyHeights, keyCharCodes, sweetSpotCenterXs, sweetSpotCenterYs, sweetSpotRadii); } public long getNativeProximityInfo() { diff --git a/java/src/com/android/inputmethod/keyboard/TypefaceUtils.java b/java/src/com/android/inputmethod/keyboard/TypefaceUtils.java new file mode 100644 index 000000000..6a54e119c --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/TypefaceUtils.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.keyboard; + +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.util.SparseArray; + +import com.android.inputmethod.latin.CollectionUtils; + +public final class TypefaceUtils { + private TypefaceUtils() { + // This utility class is not publicly instantiable. + } + + // This sparse array caches key label text height in pixel indexed by key label text size. + private static final SparseArray<Float> sTextHeightCache = CollectionUtils.newSparseArray(); + // Working variable for the following method. + private static final Rect sTextHeightBounds = new Rect(); + + public static float getCharHeight(final char[] referenceChar, final Paint paint) { + final int key = getCharGeometryCacheKey(referenceChar[0], paint); + synchronized (sTextHeightCache) { + final Float cachedValue = sTextHeightCache.get(key); + if (cachedValue != null) { + return cachedValue; + } + + paint.getTextBounds(referenceChar, 0, 1, sTextHeightBounds); + final float height = sTextHeightBounds.height(); + sTextHeightCache.put(key, height); + return height; + } + } + + // This sparse array caches key label text width in pixel indexed by key label text size. + private static final SparseArray<Float> sTextWidthCache = CollectionUtils.newSparseArray(); + // Working variable for the following method. + private static final Rect sTextWidthBounds = new Rect(); + + public static float getCharWidth(final char[] referenceChar, final Paint paint) { + final int key = getCharGeometryCacheKey(referenceChar[0], paint); + synchronized (sTextWidthCache) { + final Float cachedValue = sTextWidthCache.get(key); + if (cachedValue != null) { + return cachedValue; + } + + paint.getTextBounds(referenceChar, 0, 1, sTextWidthBounds); + final float width = sTextWidthBounds.width(); + sTextWidthCache.put(key, width); + return width; + } + } + + 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; + if (face == Typeface.DEFAULT) { + return codePointOffset + labelSize; + } else if (face == Typeface.DEFAULT_BOLD) { + return codePointOffset + labelSize + 0x1000; + } else if (face == Typeface.MONOSPACE) { + return codePointOffset + labelSize + 0x2000; + } else { + return codePointOffset + labelSize; + } + } + + public static float getLabelWidth(final String label, final Paint paint) { + final Rect textBounds = new Rect(); + paint.getTextBounds(label, 0, label.length(), textBounds); + return textBounds.width(); + } +} diff --git a/java/src/com/android/inputmethod/keyboard/internal/GesturePreviewTrail.java b/java/src/com/android/inputmethod/keyboard/internal/GesturePreviewTrail.java index e3e6d39e4..f682b518f 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/GesturePreviewTrail.java +++ b/java/src/com/android/inputmethod/keyboard/internal/GesturePreviewTrail.java @@ -37,6 +37,7 @@ import com.android.inputmethod.latin.ResizableIntArray; final class GesturePreviewTrail { private static final int DEFAULT_CAPACITY = GestureStrokeWithPreviewPoints.PREVIEW_CAPACITY; + // These three {@link ResizableIntArray}s should be synchronized by {@link #mEventTimes}. private final ResizableIntArray mXCoordinates = new ResizableIntArray(DEFAULT_CAPACITY); private final ResizableIntArray mYCoordinates = new ResizableIntArray(DEFAULT_CAPACITY); private final ResizableIntArray mEventTimes = new ResizableIntArray(DEFAULT_CAPACITY); @@ -90,7 +91,13 @@ final class GesturePreviewTrail { } public void addStroke(final GestureStrokeWithPreviewPoints stroke, final long downTime) { - final int trailSize = mEventTimes.getLength(); + synchronized (mEventTimes) { + addStrokeLocked(stroke, downTime); + } + } + + private void addStrokeLocked(final GestureStrokeWithPreviewPoints stroke, final long downTime) { + final int trailSize = mEventTimes.getLength(); stroke.appendPreviewStroke(mEventTimes, mXCoordinates, mYCoordinates); if (mEventTimes.getLength() == trailSize) { return; @@ -169,6 +176,13 @@ final class GesturePreviewTrail { */ public boolean drawGestureTrail(final Canvas canvas, final Paint paint, final Rect outBoundsRect, final Params params) { + synchronized (mEventTimes) { + return drawGestureTrailLocked(canvas, paint, outBoundsRect, params); + } + } + + private boolean drawGestureTrailLocked(final Canvas canvas, final Paint paint, + final Rect outBoundsRect, final Params params) { // Initialize bounds rectangle. outBoundsRect.setEmpty(); final int trailSize = mEventTimes.getLength(); diff --git a/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java b/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java index 2df7e5cf5..6bc6acc0f 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java +++ b/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java @@ -34,175 +34,197 @@ public final class PointerTrackerQueue { } private static final int INITIAL_CAPACITY = 10; + // Note: {@link #mExpandableArrayOfActivePointers} and {@link #mArraySize} are synchronized by + // {@link #mExpandableArrayOfActivePointers} private final ArrayList<Element> mExpandableArrayOfActivePointers = CollectionUtils.newArrayList(INITIAL_CAPACITY); private int mArraySize = 0; - public synchronized int size() { - return mArraySize; + public int size() { + synchronized (mExpandableArrayOfActivePointers) { + return mArraySize; + } } - public synchronized void add(final Element pointer) { - final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers; - final int arraySize = mArraySize; - if (arraySize < expandableArray.size()) { - expandableArray.set(arraySize, pointer); - } else { - expandableArray.add(pointer); + public void add(final Element pointer) { + synchronized (mExpandableArrayOfActivePointers) { + final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers; + final int arraySize = mArraySize; + if (arraySize < expandableArray.size()) { + expandableArray.set(arraySize, pointer); + } else { + expandableArray.add(pointer); + } + mArraySize = arraySize + 1; } - mArraySize = arraySize + 1; } - public synchronized void remove(final Element pointer) { - final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers; - final int arraySize = mArraySize; - int newSize = 0; - for (int index = 0; index < arraySize; index++) { - final Element element = expandableArray.get(index); - if (element == pointer) { + public void remove(final Element pointer) { + synchronized (mExpandableArrayOfActivePointers) { + final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers; + final int arraySize = mArraySize; + int newSize = 0; + for (int index = 0; index < arraySize; index++) { + final Element element = expandableArray.get(index); + if (element == pointer) { + if (newSize != index) { + Log.w(TAG, "Found duplicated element in remove: " + pointer); + } + continue; // Remove this element from the expandableArray. + } if (newSize != index) { - Log.w(TAG, "Found duplicated element in remove: " + pointer); + // Shift this element toward the beginning of the expandableArray. + expandableArray.set(newSize, element); } - continue; // Remove this element from the expandableArray. - } - if (newSize != index) { - // Shift this element toward the beginning of the expandableArray. - expandableArray.set(newSize, element); + newSize++; } - newSize++; + mArraySize = newSize; } - mArraySize = newSize; } - public synchronized Element getOldestElement() { - return (mArraySize == 0) ? null : mExpandableArrayOfActivePointers.get(0); + public Element getOldestElement() { + synchronized (mExpandableArrayOfActivePointers) { + return (mArraySize == 0) ? null : mExpandableArrayOfActivePointers.get(0); + } } - public synchronized void releaseAllPointersOlderThan(final Element pointer, - final long eventTime) { - if (DEBUG) { - Log.d(TAG, "releaseAllPoniterOlderThan: " + pointer + " " + this); - } - final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers; - final int arraySize = mArraySize; - int newSize, index; - for (newSize = index = 0; index < arraySize; index++) { - final Element element = expandableArray.get(index); - if (element == pointer) { - break; // Stop releasing elements. - } - if (!element.isModifier()) { - element.onPhantomUpEvent(eventTime); - continue; // Remove this element from the expandableArray. + public void releaseAllPointersOlderThan(final Element pointer, final long eventTime) { + synchronized (mExpandableArrayOfActivePointers) { + if (DEBUG) { + Log.d(TAG, "releaseAllPoniterOlderThan: " + pointer + " " + this); } - if (newSize != index) { - // Shift this element toward the beginning of the expandableArray. - expandableArray.set(newSize, element); - } - newSize++; - } - // Shift rest of the expandableArray. - int count = 0; - for (; index < arraySize; index++) { - final Element element = expandableArray.get(index); - if (element == pointer) { - if (count > 0) { - Log.w(TAG, "Found duplicated element in releaseAllPointersOlderThan: " - + pointer); + final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers; + final int arraySize = mArraySize; + int newSize, index; + for (newSize = index = 0; index < arraySize; index++) { + final Element element = expandableArray.get(index); + if (element == pointer) { + break; // Stop releasing elements. + } + if (!element.isModifier()) { + element.onPhantomUpEvent(eventTime); + continue; // Remove this element from the expandableArray. + } + if (newSize != index) { + // Shift this element toward the beginning of the expandableArray. + expandableArray.set(newSize, element); } - count++; - } - if (newSize != index) { - expandableArray.set(newSize, expandableArray.get(index)); newSize++; } + // Shift rest of the expandableArray. + int count = 0; + for (; index < arraySize; index++) { + final Element element = expandableArray.get(index); + if (element == pointer) { + if (count > 0) { + Log.w(TAG, "Found duplicated element in releaseAllPointersOlderThan: " + + pointer); + } + count++; + } + if (newSize != index) { + expandableArray.set(newSize, expandableArray.get(index)); + newSize++; + } + } + mArraySize = newSize; } - mArraySize = newSize; } public void releaseAllPointers(final long eventTime) { releaseAllPointersExcept(null, eventTime); } - public synchronized void releaseAllPointersExcept(final Element pointer, - final long eventTime) { - if (DEBUG) { - if (pointer == null) { - Log.d(TAG, "releaseAllPoniters: " + this); - } else { - Log.d(TAG, "releaseAllPoniterExcept: " + pointer + " " + this); - } - } - final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers; - final int arraySize = mArraySize; - int newSize = 0, count = 0; - for (int index = 0; index < arraySize; index++) { - final Element element = expandableArray.get(index); - if (element == pointer) { - if (count > 0) { - Log.w(TAG, "Found duplicated element in releaseAllPointersExcept: " + pointer); + public void releaseAllPointersExcept(final Element pointer, final long eventTime) { + synchronized (mExpandableArrayOfActivePointers) { + if (DEBUG) { + if (pointer == null) { + Log.d(TAG, "releaseAllPoniters: " + this); + } else { + Log.d(TAG, "releaseAllPoniterExcept: " + pointer + " " + this); } - count++; - } else { - element.onPhantomUpEvent(eventTime); - continue; // Remove this element from the expandableArray. } - if (newSize != index) { - // Shift this element toward the beginning of the expandableArray. - expandableArray.set(newSize, element); + final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers; + final int arraySize = mArraySize; + int newSize = 0, count = 0; + for (int index = 0; index < arraySize; index++) { + final Element element = expandableArray.get(index); + if (element == pointer) { + if (count > 0) { + Log.w(TAG, "Found duplicated element in releaseAllPointersExcept: " + + pointer); + } + count++; + } else { + element.onPhantomUpEvent(eventTime); + continue; // Remove this element from the expandableArray. + } + if (newSize != index) { + // Shift this element toward the beginning of the expandableArray. + expandableArray.set(newSize, element); + } + newSize++; } - newSize++; + mArraySize = newSize; } - mArraySize = newSize; } - public synchronized boolean hasModifierKeyOlderThan(final Element pointer) { - final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers; - final int arraySize = mArraySize; - for (int index = 0; index < arraySize; index++) { - final Element element = expandableArray.get(index); - if (element == pointer) { - return false; // Stop searching modifier key. - } - if (element.isModifier()) { - return true; + public boolean hasModifierKeyOlderThan(final Element pointer) { + synchronized (mExpandableArrayOfActivePointers) { + final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers; + final int arraySize = mArraySize; + for (int index = 0; index < arraySize; index++) { + final Element element = expandableArray.get(index); + if (element == pointer) { + return false; // Stop searching modifier key. + } + if (element.isModifier()) { + return true; + } } + return false; } - return false; } - public synchronized boolean isAnyInSlidingKeyInput() { - final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers; - final int arraySize = mArraySize; - for (int index = 0; index < arraySize; index++) { - final Element element = expandableArray.get(index); - if (element.isInSlidingKeyInput()) { - return true; + public boolean isAnyInSlidingKeyInput() { + synchronized (mExpandableArrayOfActivePointers) { + final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers; + final int arraySize = mArraySize; + for (int index = 0; index < arraySize; index++) { + final Element element = expandableArray.get(index); + if (element.isInSlidingKeyInput()) { + return true; + } } + return false; } - return false; } - public synchronized void cancelAllPointerTracker() { - final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers; - final int arraySize = mArraySize; - for (int index = 0; index < arraySize; index++) { - final Element element = expandableArray.get(index); - element.cancelTracking(); + public void cancelAllPointerTracker() { + synchronized (mExpandableArrayOfActivePointers) { + final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers; + final int arraySize = mArraySize; + for (int index = 0; index < arraySize; index++) { + final Element element = expandableArray.get(index); + element.cancelTracking(); + } } } @Override - public synchronized String toString() { - final StringBuilder sb = new StringBuilder(); - final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers; - final int arraySize = mArraySize; - for (int index = 0; index < arraySize; index++) { - final Element element = expandableArray.get(index); - if (sb.length() > 0) - sb.append(" "); - sb.append(element.toString()); + public String toString() { + synchronized (mExpandableArrayOfActivePointers) { + final StringBuilder sb = new StringBuilder(); + final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers; + final int arraySize = mArraySize; + for (int index = 0; index < arraySize; index++) { + final Element element = expandableArray.get(index); + if (sb.length() > 0) { + sb.append(" "); + } + sb.append(element.toString()); + } + return "[" + sb.toString() + "]"; } - return "[" + sb.toString() + "]"; } } diff --git a/java/src/com/android/inputmethod/latin/CapsModeUtils.java b/java/src/com/android/inputmethod/latin/CapsModeUtils.java index 1012cd519..4b8d1ac11 100644 --- a/java/src/com/android/inputmethod/latin/CapsModeUtils.java +++ b/java/src/com/android/inputmethod/latin/CapsModeUtils.java @@ -41,7 +41,7 @@ public final class CapsModeUtils { if (WordComposer.CAPS_MODE_AUTO_SHIFT_LOCKED == capitalizeMode) { return s.toUpperCase(locale); } else if (WordComposer.CAPS_MODE_AUTO_SHIFTED == capitalizeMode) { - return StringUtils.toTitleCase(s, locale); + return StringUtils.capitalizeFirstCodePoint(s, locale); } else { return s; } diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index 56b1c786e..0f1f14957 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -1540,7 +1540,8 @@ public final class LatinIME extends InputMethodService implements KeyboardAction } } else { final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor(); - if (mSettings.getCurrent().isUsuallyFollowedBySpace(codePointBeforeCursor)) { + if (Character.isLetter(codePointBeforeCursor) + || mSettings.getCurrent().isUsuallyFollowedBySpace(codePointBeforeCursor)) { mSpaceState = SPACE_STATE_PHANTOM; } } @@ -1551,7 +1552,8 @@ public final class LatinIME extends InputMethodService implements KeyboardAction private static final class BatchInputUpdater implements Handler.Callback { private final Handler mHandler; private LatinIME mLatinIme; - private boolean mInBatchInput; // synchronized using "this". + private final Object mLock = new Object(); + private boolean mInBatchInput; // synchronized using {@link #mLock}. private BatchInputUpdater() { final HandlerThread handlerThread = new HandlerThread( @@ -1582,21 +1584,25 @@ public final class LatinIME extends InputMethodService implements KeyboardAction } // Run in the UI thread. - public synchronized void onStartBatchInput(final LatinIME latinIme) { - mHandler.removeMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP); - mLatinIme = latinIme; - mInBatchInput = true; + public void onStartBatchInput(final LatinIME latinIme) { + synchronized (mLock) { + mHandler.removeMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP); + mLatinIme = latinIme; + mInBatchInput = true; + } } // Run in the Handler thread. - private synchronized void updateBatchInput(final InputPointers batchPointers) { - if (!mInBatchInput) { - // Batch input has ended or canceled while the message was being delivered. - return; + private void updateBatchInput(final InputPointers batchPointers) { + synchronized (mLock) { + if (!mInBatchInput) { + // Batch input has ended or canceled while the message was being delivered. + return; + } + final SuggestedWords suggestedWords = getSuggestedWordsGestureLocked(batchPointers); + mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip( + suggestedWords, false /* dismissGestureFloatingPreviewText */); } - final SuggestedWords suggestedWords = getSuggestedWordsGestureLocked(batchPointers); - mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip( - suggestedWords, false /* dismissGestureFloatingPreviewText */); } // Run in the UI thread. @@ -1609,19 +1615,23 @@ public final class LatinIME extends InputMethodService implements KeyboardAction .sendToTarget(); } - public synchronized void onCancelBatchInput() { - mInBatchInput = false; - mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip( - SuggestedWords.EMPTY, true /* dismissGestureFloatingPreviewText */); + public void onCancelBatchInput() { + synchronized (mLock) { + mInBatchInput = false; + mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip( + SuggestedWords.EMPTY, true /* dismissGestureFloatingPreviewText */); + } } // Run in the UI thread. - public synchronized SuggestedWords onEndBatchInput(final InputPointers batchPointers) { - mInBatchInput = false; - final SuggestedWords suggestedWords = getSuggestedWordsGestureLocked(batchPointers); - mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip( - suggestedWords, true /* dismissGestureFloatingPreviewText */); - return suggestedWords; + public SuggestedWords onEndBatchInput(final InputPointers batchPointers) { + synchronized (mLock) { + mInBatchInput = false; + final SuggestedWords suggestedWords = getSuggestedWordsGestureLocked(batchPointers); + mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip( + suggestedWords, true /* dismissGestureFloatingPreviewText */); + return suggestedWords; + } } // {@link LatinIME#getSuggestedWords(int)} method calls with same session id have to diff --git a/java/src/com/android/inputmethod/latin/SettingsActivity.java b/java/src/com/android/inputmethod/latin/SettingsActivity.java index 99b572e06..37ac2e35c 100644 --- a/java/src/com/android/inputmethod/latin/SettingsActivity.java +++ b/java/src/com/android/inputmethod/latin/SettingsActivity.java @@ -25,7 +25,10 @@ public final class SettingsActivity extends PreferenceActivity { @Override public Intent getIntent() { final Intent intent = super.getIntent(); - intent.putExtra(EXTRA_SHOW_FRAGMENT, DEFAULT_FRAGMENT); + final String fragment = intent.getStringExtra(EXTRA_SHOW_FRAGMENT); + if (fragment == null) { + intent.putExtra(EXTRA_SHOW_FRAGMENT, DEFAULT_FRAGMENT); + } intent.putExtra(EXTRA_NO_HEADERS, true); return intent; } diff --git a/java/src/com/android/inputmethod/latin/StringUtils.java b/java/src/com/android/inputmethod/latin/StringUtils.java index 59ad28fc9..3ca209d34 100644 --- a/java/src/com/android/inputmethod/latin/StringUtils.java +++ b/java/src/com/android/inputmethod/latin/StringUtils.java @@ -106,10 +106,19 @@ public final class StringUtils { } } - public static String toTitleCase(final String s, final Locale locale) { + public static String capitalizeFirstCodePoint(final String s, final Locale locale) { if (s.length() <= 1) { - // TODO: is this really correct? Shouldn't this be s.toUpperCase()? - return s; + return s.toUpperCase(locale); + } + // Please refer to the comment below in + // {@link #capitalizeFirstAndDowncaseRest(String,Locale)} as this has the same shortcomings + final int cutoff = s.offsetByCodePoints(0, 1); + return s.substring(0, cutoff).toUpperCase(locale) + s.substring(cutoff); + } + + public static String capitalizeFirstAndDowncaseRest(final String s, final Locale locale) { + if (s.length() <= 1) { + return s.toUpperCase(locale); } // TODO: fix the bugs below // - This does not work for Greek, because it returns upper case instead of title case. @@ -213,4 +222,75 @@ public final class StringUtils { if (1 == capsCount) return CAPITALIZE_FIRST; return (letterCount == capsCount ? CAPITALIZE_ALL : CAPITALIZE_NONE); } + + public static boolean isIdenticalAfterUpcase(final String text) { + final int len = text.length(); + for (int i = 0; i < len; i = text.offsetByCodePoints(i, 1)) { + final int codePoint = text.codePointAt(i); + if (Character.isLetter(codePoint) && !Character.isUpperCase(codePoint)) { + return false; + } + } + return true; + } + + public static boolean isIdenticalAfterDowncase(final String text) { + final int len = text.length(); + for (int i = 0; i < len; i = text.offsetByCodePoints(i, 1)) { + final int codePoint = text.codePointAt(i); + if (Character.isLetter(codePoint) && !Character.isLowerCase(codePoint)) { + return false; + } + } + return true; + } + + public static boolean isIdenticalAfterCapitalizeEachWord(final String text, + final String separators) { + boolean needCapsNext = true; + final int len = text.length(); + for (int i = 0; i < len; i = text.offsetByCodePoints(i, 1)) { + final int codePoint = text.codePointAt(i); + if (Character.isLetter(codePoint)) { + if ((needCapsNext && !Character.isUpperCase(codePoint)) + || (!needCapsNext && !Character.isLowerCase(codePoint))) { + return false; + } + } + // We need a capital letter next if this is a separator. + needCapsNext = (-1 != separators.indexOf(codePoint)); + } + return true; + } + + // TODO: like capitalizeFirst*, this does not work perfectly for Dutch because of the IJ digraph + // which should be capitalized together in *some* cases. + public static String capitalizeEachWord(final String text, final String separators, + final Locale locale) { + final StringBuilder builder = new StringBuilder(); + boolean needCapsNext = true; + final int len = text.length(); + for (int i = 0; i < len; i = text.offsetByCodePoints(i, 1)) { + final String nextChar = text.substring(i, text.offsetByCodePoints(i, 1)); + if (needCapsNext) { + builder.append(nextChar.toUpperCase(locale)); + } else { + builder.append(nextChar.toLowerCase(locale)); + } + // We need a capital letter next if this is a separator. + needCapsNext = (-1 != separators.indexOf(nextChar.codePointAt(0))); + } + return builder.toString(); + } + + public static boolean containsAny(final String string, final String separators) { + final int len = separators.length(); + for (int i = 0; i < len; i = separators.offsetByCodePoints(i, 1)) { + final int separator = separators.codePointAt(i); + if (-1 != string.indexOf(separator)) { + return true; + } + } + return false; + } } diff --git a/java/src/com/android/inputmethod/latin/SubtypeLocale.java b/java/src/com/android/inputmethod/latin/SubtypeLocale.java index 5e28cc2d0..4d88ecc0c 100644 --- a/java/src/com/android/inputmethod/latin/SubtypeLocale.java +++ b/java/src/com/android/inputmethod/latin/SubtypeLocale.java @@ -183,7 +183,7 @@ public final class SubtypeLocale { final Locale locale = LocaleUtils.constructLocaleFromString(localeString); displayName = locale.getDisplayName(displayLocale); } - return StringUtils.toTitleCase(displayName, displayLocale); + return StringUtils.capitalizeFirstCodePoint(displayName, displayLocale); } // InputMethodSubtype's display name in its locale. @@ -243,7 +243,7 @@ public final class SubtypeLocale { } } }; - return StringUtils.toTitleCase( + return StringUtils.capitalizeFirstCodePoint( getSubtypeName.runInLocale(sResources, displayLocale), displayLocale); } diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java index 975664dca..6464bd0d7 100644 --- a/java/src/com/android/inputmethod/latin/Suggest.java +++ b/java/src/com/android/inputmethod/latin/Suggest.java @@ -394,7 +394,7 @@ public final class Suggest { if (isAllUpperCase) { sb.append(wordInfo.mWord.toUpperCase(locale)); } else if (isFirstCharCapitalized) { - sb.append(StringUtils.toTitleCase(wordInfo.mWord, locale)); + sb.append(StringUtils.capitalizeFirstCodePoint(wordInfo.mWord, locale)); } else { sb.append(wordInfo.mWord); } diff --git a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java index e7c7e2b8a..17d281518 100644 --- a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java +++ b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java @@ -647,7 +647,7 @@ public final class FusionDictionary implements Iterable<Word> { if (index < codePoints.length) return null; if (!currentGroup.isTerminal()) return null; - if (DBG && !codePoints.equals(checker.toString())) return null; + if (DBG && !string.equals(checker.toString())) return null; return currentGroup; } @@ -853,16 +853,19 @@ public final class FusionDictionary implements Iterable<Word> { if (currentPos.pos.hasNext()) { final CharGroup currentGroup = currentPos.pos.next(); currentPos.length = mCurrentString.length(); - for (int i : currentGroup.mChars) + for (int i : currentGroup.mChars) { mCurrentString.append(Character.toChars(i)); + } if (null != currentGroup.mChildren) { currentPos = new Position(currentGroup.mChildren.mData); + currentPos.length = mCurrentString.length(); mPositions.addLast(currentPos); } - if (currentGroup.mFrequency >= 0) + if (currentGroup.mFrequency >= 0) { return new Word(mCurrentString.toString(), currentGroup.mFrequency, currentGroup.mShortcutTargets, currentGroup.mBigrams, currentGroup.mIsNotAWord, currentGroup.mIsBlacklistEntry); + } } else { mPositions.removeLast(); currentPos = mPositions.getLast(); diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java index 8d3b062ff..2d0a89bb3 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java @@ -330,7 +330,7 @@ public final class AndroidSpellCheckerService extends SpellCheckerService } else if (StringUtils.CAPITALIZE_FIRST == capitalizeType) { for (int i = 0; i < mSuggestions.size(); ++i) { // Likewise - mSuggestions.set(i, StringUtils.toTitleCase( + mSuggestions.set(i, StringUtils.capitalizeFirstCodePoint( mSuggestions.get(i).toString(), locale)); } } @@ -403,11 +403,7 @@ public final class AndroidSpellCheckerService extends SpellCheckerService public DictAndProximity createDictAndProximity(final Locale locale) { final int script = getScriptFromLocale(locale); - final ProximityInfo proximityInfo = ProximityInfo.createSpellCheckerProximityInfo( - SpellCheckerProximityInfo.getProximityForScript(script), - SpellCheckerProximityInfo.ROW_SIZE, - SpellCheckerProximityInfo.PROXIMITY_GRID_WIDTH, - SpellCheckerProximityInfo.PROXIMITY_GRID_HEIGHT); + final ProximityInfo proximityInfo = new SpellCheckerProximityInfo(script); final DictionaryCollection dictionaryCollection = DictionaryFactory.createMainDictionaryFromManager(this, locale, true /* useFullEditDistance */); diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java index b15063235..96b2c818d 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java @@ -226,7 +226,7 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { // If the lower case version is not in the dictionary, it's still possible // that we have an all-caps version of a word that needs to be capitalized // according to the dictionary. E.g. "GERMANS" only exists in the dictionary as "Germans". - return dict.isValidWord(StringUtils.toTitleCase(lowerCaseText, mLocale)); + return dict.isValidWord(StringUtils.capitalizeFirstAndDowncaseRest(lowerCaseText, mLocale)); } // Note : this must be reentrant diff --git a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java index 49dca21e6..0c480eaba 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java @@ -16,53 +16,40 @@ package com.android.inputmethod.latin.spellcheck; -import com.android.inputmethod.annotations.UsedForTesting; +import android.util.SparseIntArray; + import com.android.inputmethod.keyboard.ProximityInfo; -import com.android.inputmethod.latin.CollectionUtils; import com.android.inputmethod.latin.Constants; -import java.util.TreeMap; +public final class SpellCheckerProximityInfo extends ProximityInfo { + public SpellCheckerProximityInfo(final int script) { + super(getProximityForScript(script), PROXIMITY_GRID_WIDTH, PROXIMITY_GRID_HEIGHT); + } -public final class SpellCheckerProximityInfo { - @UsedForTesting - final public static int NUL = Constants.NOT_A_CODE; + private static final int NUL = Constants.NOT_A_CODE; // This must be the same as MAX_PROXIMITY_CHARS_SIZE else it will not work inside // native code - this value is passed at creation of the binary object and reused // as the size of the passed array afterwards so they can't be different. - final public static int ROW_SIZE = ProximityInfo.MAX_PROXIMITY_CHARS_SIZE; + private static final int ROW_SIZE = ProximityInfo.MAX_PROXIMITY_CHARS_SIZE; // The number of keys in a row of the grid used by the spell checker. - final public static int PROXIMITY_GRID_WIDTH = 11; + private static final int PROXIMITY_GRID_WIDTH = 11; // The number of rows in the grid used by the spell checker. - final public static int PROXIMITY_GRID_HEIGHT = 3; + private static final int PROXIMITY_GRID_HEIGHT = 3; - final private static int NOT_AN_INDEX = -1; - final public static int NOT_A_COORDINATE_PAIR = -1; + private static final int NOT_AN_INDEX = -1; + public static final int NOT_A_COORDINATE_PAIR = -1; // Helper methods - final protected static void buildProximityIndices(final int[] proximity, - final TreeMap<Integer, Integer> indices) { - for (int i = 0; i < proximity.length; i += ROW_SIZE) { - if (NUL != proximity[i]) indices.put(proximity[i], i / ROW_SIZE); + static void buildProximityIndices(final int[] proximity, final int rowSize, + final SparseIntArray indices) { + for (int i = 0; i < proximity.length; i += rowSize) { + if (NUL != proximity[i]) indices.put(proximity[i], i / rowSize); } } - final protected static int computeIndex(final int characterCode, - final TreeMap<Integer, Integer> indices) { - final Integer result = indices.get(characterCode); - if (null == result) return NOT_AN_INDEX; - return result; - } private static final class Latin { - // This is a map from the code point to the index in the PROXIMITY array. - // At the time the native code to read the binary dictionary needs the proximity info be - // passed as a flat array spaced by MAX_PROXIMITY_CHARS_SIZE columns, one for each input - // character. - // Since we need to build such an array, we want to be able to search in our big proximity - // data quickly by character, and a map is probably the best way to do this. - final private static TreeMap<Integer, Integer> INDICES = CollectionUtils.newTreeMap(); - // The proximity here is the union of // - the proximity for a QWERTY keyboard. // - the proximity for an AZERTY keyboard. @@ -79,7 +66,7 @@ public final class SpellCheckerProximityInfo { a s d f g h j k l z x c v b n m */ - final static int[] PROXIMITY = { + static final int[] PROXIMITY = { // Proximity for row 1. This must have exactly ROW_SIZE entries for each letter, // and exactly PROXIMITY_GRID_WIDTH letters for a row. Pad with NUL's. // The number of rows must be exactly PROXIMITY_GRID_HEIGHT. @@ -121,16 +108,21 @@ public final class SpellCheckerProximityInfo { NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, }; + + // This is a mapping array from the code point to the index in the PROXIMITY array. + // When we check the spelling of a word, we need to pass (x,y) coordinates to the native + // code for each letter of the word. These are most easily computed from the index in the + // PROXIMITY array. Since we'll need to do that very often, the index lookup from the code + // point needs to be as fast as possible, and a map is probably the best way to do this. + // To avoid unnecessary boxing conversion to Integer, here we use SparseIntArray. + static final SparseIntArray INDICES = new SparseIntArray(PROXIMITY.length / ROW_SIZE); + static { - buildProximityIndices(PROXIMITY, INDICES); - } - static int getIndexOf(int characterCode) { - return computeIndex(characterCode, INDICES); + buildProximityIndices(PROXIMITY, ROW_SIZE, INDICES); } } private static final class Cyrillic { - final private static TreeMap<Integer, Integer> INDICES = CollectionUtils.newTreeMap(); // TODO: The following table is solely based on the keyboard layout. Consult with Russian // speakers on commonly misspelled words/letters. /* @@ -207,7 +199,7 @@ public final class SpellCheckerProximityInfo { private static final int CY_SOFT_SIGN = '\u044C'; // ь private static final int CY_BE = '\u0431'; // б private static final int CY_YU = '\u044E'; // ю - final static int[] PROXIMITY = { + static final int[] PROXIMITY = { // Proximity for row 1. This must have exactly ROW_SIZE entries for each letter, // and exactly PROXIMITY_GRID_WIDTH letters for a row. Pad with NUL's. // The number of rows must be exactly PROXIMITY_GRID_HEIGHT. @@ -280,16 +272,15 @@ public final class SpellCheckerProximityInfo { NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, }; + + static final SparseIntArray INDICES = new SparseIntArray(PROXIMITY.length / ROW_SIZE); + static { - buildProximityIndices(PROXIMITY, INDICES); - } - static int getIndexOf(int characterCode) { - return computeIndex(characterCode, INDICES); + buildProximityIndices(PROXIMITY, ROW_SIZE, INDICES); } } private static final class Greek { - final private static TreeMap<Integer, Integer> INDICES = CollectionUtils.newTreeMap(); // TODO: The following table is solely based on the keyboard layout. Consult with Greek // speakers on commonly misspelled words/letters. /* @@ -354,7 +345,7 @@ public final class SpellCheckerProximityInfo { private static final int GR_BETA = '\u03B2'; // β private static final int GR_NU = '\u03BD'; // ν private static final int GR_MU = '\u03BC'; // μ - final static int[] PROXIMITY = { + static final int[] PROXIMITY = { // Proximity for row 1. This must have exactly ROW_SIZE entries for each letter, // and exactly PROXIMITY_GRID_WIDTH letters for a row. Pad with NUL's. // The number of rows must be exactly PROXIMITY_GRID_HEIGHT. @@ -419,37 +410,37 @@ public final class SpellCheckerProximityInfo { NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, }; + + static final SparseIntArray INDICES = new SparseIntArray(PROXIMITY.length / ROW_SIZE); + static { - buildProximityIndices(PROXIMITY, INDICES); - } - static int getIndexOf(int characterCode) { - return computeIndex(characterCode, INDICES); + buildProximityIndices(PROXIMITY, ROW_SIZE, INDICES); } } - public static int[] getProximityForScript(final int script) { + private static int[] getProximityForScript(final int script) { switch (script) { - case AndroidSpellCheckerService.SCRIPT_LATIN: - return Latin.PROXIMITY; - case AndroidSpellCheckerService.SCRIPT_CYRILLIC: - return Cyrillic.PROXIMITY; - case AndroidSpellCheckerService.SCRIPT_GREEK: - return Greek.PROXIMITY; - default: - throw new RuntimeException("Wrong script supplied: " + script); + case AndroidSpellCheckerService.SCRIPT_LATIN: + return Latin.PROXIMITY; + case AndroidSpellCheckerService.SCRIPT_CYRILLIC: + return Cyrillic.PROXIMITY; + case AndroidSpellCheckerService.SCRIPT_GREEK: + return Greek.PROXIMITY; + default: + throw new RuntimeException("Wrong script supplied: " + script); } } private static int getIndexOfCodeForScript(final int codePoint, final int script) { switch (script) { - case AndroidSpellCheckerService.SCRIPT_LATIN: - return Latin.getIndexOf(codePoint); - case AndroidSpellCheckerService.SCRIPT_CYRILLIC: - return Cyrillic.getIndexOf(codePoint); - case AndroidSpellCheckerService.SCRIPT_GREEK: - return Greek.getIndexOf(codePoint); - default: - throw new RuntimeException("Wrong script supplied: " + script); + case AndroidSpellCheckerService.SCRIPT_LATIN: + return Latin.INDICES.get(codePoint, NOT_AN_INDEX); + case AndroidSpellCheckerService.SCRIPT_CYRILLIC: + return Cyrillic.INDICES.get(codePoint, NOT_AN_INDEX); + case AndroidSpellCheckerService.SCRIPT_GREEK: + return Greek.INDICES.get(codePoint, NOT_AN_INDEX); + default: + throw new RuntimeException("Wrong script supplied: " + script); } } diff --git a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java index ed408bb3c..3037669c0 100644 --- a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java +++ b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java @@ -16,12 +16,14 @@ package com.android.inputmethod.latin.suggestions; +import android.content.Context; import android.content.res.Resources; import android.graphics.Paint; import android.graphics.drawable.Drawable; import com.android.inputmethod.keyboard.Key; import com.android.inputmethod.keyboard.Keyboard; +import com.android.inputmethod.keyboard.TypefaceUtils; import com.android.inputmethod.keyboard.internal.KeyboardBuilder; import com.android.inputmethod.keyboard.internal.KeyboardIconsSet; import com.android.inputmethod.keyboard.internal.KeyboardParams; @@ -50,16 +52,12 @@ public final class MoreSuggestions extends Keyboard { super(); } - // TODO: Remove {@link MoreSuggestionsView} argument. public int layout(final SuggestedWords suggestions, final int fromPos, final int maxWidth, - final int minWidth, final int maxRow, final MoreSuggestionsView view) { + final int minWidth, final int maxRow, final Paint paint, final Resources res) { clearKeys(); - final Resources res = view.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(); + final float padding = res.getDimension(R.dimen.more_suggestions_key_horizontal_padding); int row = 0; int pos = fromPos, rowStartPos = fromPos; @@ -67,7 +65,7 @@ public final class MoreSuggestions extends Keyboard { while (pos < size) { final String word = suggestions.getWord(pos); // TODO: Should take care of text x-scaling. - mWidths[pos] = (int)view.getLabelWidth(word, paint) + padding; + mWidths[pos] = (int)(TypefaceUtils.getLabelWidth(word, paint) + padding); final int numColumn = pos - rowStartPos + 1; final int columnWidth = (maxWidth - mDividerWidth * (numColumn - 1)) / numColumn; @@ -169,8 +167,8 @@ public final class MoreSuggestions extends Keyboard { private int mFromPos; private int mToPos; - public Builder(final MoreSuggestionsView paneView) { - super(paneView.getContext(), new MoreSuggestionsParam()); + public Builder(final Context context, final MoreSuggestionsView paneView) { + super(context, new MoreSuggestionsParam()); mPaneView = paneView; } @@ -183,7 +181,7 @@ public final class MoreSuggestions extends Keyboard { mPaneView.updateKeyboardGeometry(mParams.mDefaultRowHeight); final int count = mParams.layout(suggestions, fromPos, maxWidth, minWidth, maxRow, - mPaneView); + mPaneView.newLabelPaint(null /* key */), mResources); mFromPos = fromPos; mToPos = fromPos + count; mSuggestions = suggestions; diff --git a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java index 438820d17..94715cd84 100644 --- a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java +++ b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java @@ -43,7 +43,7 @@ public final class MoreSuggestionsView extends MoreKeysKeyboardView { } public void updateKeyboardGeometry(final int keyHeight) { - mKeyDrawParams.updateParams(keyHeight, mKeyVisualAttributes); + updateKeyDrawParams(keyHeight); } @Override diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java index eeaf828a7..4ef36fa46 100644 --- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java +++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java @@ -596,7 +596,7 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick mMoreSuggestionsContainer = inflater.inflate(R.layout.more_suggestions, null); mMoreSuggestionsView = (MoreSuggestionsView)mMoreSuggestionsContainer .findViewById(R.id.more_suggestions_view); - mMoreSuggestionsBuilder = new MoreSuggestions.Builder(mMoreSuggestionsView); + mMoreSuggestionsBuilder = new MoreSuggestions.Builder(context, mMoreSuggestionsView); final Resources res = context.getResources(); mMoreSuggestionsModalTolerance = res.getDimensionPixelOffset( diff --git a/java/src/com/android/inputmethod/research/BootBroadcastReceiver.java b/java/src/com/android/inputmethod/research/BootBroadcastReceiver.java index c5f095919..4f86526a7 100644 --- a/java/src/com/android/inputmethod/research/BootBroadcastReceiver.java +++ b/java/src/com/android/inputmethod/research/BootBroadcastReceiver.java @@ -25,9 +25,10 @@ import android.content.Intent; */ public final class BootBroadcastReceiver extends BroadcastReceiver { @Override - public void onReceive(Context context, Intent intent) { + public void onReceive(final Context context, final Intent intent) { if (intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) { - ResearchLogger.scheduleUploadingService(context); + UploaderService.cancelAndRescheduleUploadingService(context, + true /* needsRescheduling */); } } } diff --git a/java/src/com/android/inputmethod/research/MotionEventReader.java b/java/src/com/android/inputmethod/research/MotionEventReader.java index e1cc2da73..fbfd9b531 100644 --- a/java/src/com/android/inputmethod/research/MotionEventReader.java +++ b/java/src/com/android/inputmethod/research/MotionEventReader.java @@ -22,6 +22,7 @@ import android.view.MotionEvent; import android.view.MotionEvent.PointerCoords; import android.view.MotionEvent.PointerProperties; +import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.latin.define.ProductionFlag; import java.io.BufferedReader; @@ -64,6 +65,7 @@ public class MotionEventReader { return replayData; } + @UsedForTesting static class ReplayData { final ArrayList<Integer> mActions = new ArrayList<Integer>(); final ArrayList<PointerProperties[]> mPointerPropertiesArrays @@ -134,6 +136,7 @@ public class MotionEventReader { * }, * </pre> */ + @UsedForTesting /* package for test */ void readLogStatement(final JsonReader jsonReader, final ReplayData replayData) throws IOException { String logStatementType = null; diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java index fa124f3ba..c05de0992 100644 --- a/java/src/com/android/inputmethod/research/ResearchLogger.java +++ b/java/src/com/android/inputmethod/research/ResearchLogger.java @@ -20,16 +20,13 @@ import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.KEYBOAR import android.accounts.Account; import android.accounts.AccountManager; -import android.app.AlarmManager; import android.app.AlertDialog; import android.app.Dialog; -import android.app.PendingIntent; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnCancelListener; import android.content.Intent; import android.content.SharedPreferences; -import android.content.SharedPreferences.Editor; import android.content.pm.PackageInfo; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources; @@ -74,22 +71,17 @@ import com.android.inputmethod.latin.SuggestedWords; import com.android.inputmethod.latin.define.ProductionFlag; import com.android.inputmethod.research.MotionEventReader.ReplayData; -import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; -import java.io.InputStreamReader; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; import java.nio.charset.Charset; -import java.text.SimpleDateFormat; import java.util.ArrayList; -import java.util.Date; import java.util.List; -import java.util.Locale; import java.util.Random; -import java.util.UUID; +import java.util.regex.Pattern; /** * Logs the use of the LatinIME keyboard. @@ -254,7 +246,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang mUploadNowIntent = new Intent(mLatinIME, UploaderService.class); mUploadNowIntent.putExtra(UploaderService.EXTRA_UPLOAD_UNCONDITIONALLY, true); if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - scheduleUploadingService(mLatinIME); + UploaderService.cancelAndRescheduleUploadingService(mLatinIME, + true /* needsRescheduling */); } mReplayer.setKeyboardSwitcher(keyboardSwitcher); } @@ -268,25 +261,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang ResearchSettings.writeResearchLastDirCleanupTime(mPrefs, now); } - /** - * Arrange for the UploaderService to be run on a regular basis. - * - * Any existing scheduled invocation of UploaderService is removed and rescheduled. This may - * cause problems if this method is called often and frequent updates are required, but since - * the user will likely be sleeping at some point, if the interval is less that the expected - * sleep duration and this method is not called during that time, the service should be invoked - * at some point. - */ - public static void scheduleUploadingService(Context context) { - final Intent intent = new Intent(context, UploaderService.class); - final PendingIntent pendingIntent = PendingIntent.getService(context, 0, intent, 0); - final AlarmManager manager = - (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - manager.cancel(pendingIntent); - manager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, - UploaderService.RUN_INTERVAL, UploaderService.RUN_INTERVAL, pendingIntent); - } - public void mainKeyboardView_onAttachedToWindow(final MainKeyboardView mainKeyboardView) { mMainKeyboardView = mainKeyboardView; maybeShowSplashScreen(); @@ -1092,7 +1066,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang new LogStatement("LatinImeOnStartInputViewInternal", false, false, "uuid", "packageName", "inputType", "imeOptions", "fieldId", "display", "model", "prefs", "versionCode", "versionName", "outputFormatVersion", "logEverything", - "isUsingDevelopmentOnlyDiagnosticsDebug"); + "isDevTeamBuild"); public static void latinIME_onStartInputViewInternal(final EditorInfo editorInfo, final SharedPreferences prefs) { final ResearchLogger researchLogger = getInstance(); @@ -1114,13 +1088,32 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang Integer.toHexString(editorInfo.imeOptions), editorInfo.fieldId, Build.DISPLAY, Build.MODEL, prefs, versionCode, versionName, OUTPUT_FORMAT_VERSION, IS_LOGGING_EVERYTHING, - ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG); - } catch (NameNotFoundException e) { - e.printStackTrace(); + researchLogger.isDevTeamBuild()); + // Commit the logUnit so the LatinImeOnStartInputViewInternal event is in its own + // logUnit at the beginning of the log. + researchLogger.commitCurrentLogUnit(); + } catch (final NameNotFoundException e) { + Log.e(TAG, "NameNotFound", e); } } } + // TODO: Update this heuristic pattern to something more reliable. Developer builds tend to + // have the developer name and year embedded. + private static final Pattern developerBuildRegex = Pattern.compile("[A-Za-z]\\.20[1-9]"); + private boolean isDevTeamBuild() { + try { + final PackageInfo packageInfo; + packageInfo = mLatinIME.getPackageManager().getPackageInfo(mLatinIME.getPackageName(), + 0); + final String versionName = packageInfo.versionName; + return !(developerBuildRegex.matcher(versionName).find()); + } catch (final NameNotFoundException e) { + Log.e(TAG, "Could not determine package name", e); + return false; + } + } + /** * Log a change in preferences. * diff --git a/java/src/com/android/inputmethod/research/UploaderService.java b/java/src/com/android/inputmethod/research/UploaderService.java index 6a9f5c1f4..6a9717b7c 100644 --- a/java/src/com/android/inputmethod/research/UploaderService.java +++ b/java/src/com/android/inputmethod/research/UploaderService.java @@ -18,6 +18,8 @@ package com.android.inputmethod.research; import android.app.AlarmManager; import android.app.IntentService; +import android.app.PendingIntent; +import android.content.Context; import android.content.Intent; import android.os.Bundle; @@ -43,11 +45,17 @@ public final class UploaderService extends IntentService { @Override protected void onHandleIntent(final Intent intent) { + // We may reach this point either because the alarm fired, or because the system explicitly + // requested that an Upload occur. In the latter case, we want to cancel the alarm in case + // it's about to fire. + cancelAndRescheduleUploadingService(this, false /* needsRescheduling */); + final Uploader uploader = new Uploader(this); if (!uploader.isPossibleToUpload()) return; if (isUploadingUnconditionally(intent.getExtras()) || uploader.isConvenientToUpload()) { uploader.doUpload(); } + cancelAndRescheduleUploadingService(this, true /* needsRescheduling */); } private boolean isUploadingUnconditionally(final Bundle bundle) { @@ -57,4 +65,42 @@ public final class UploaderService extends IntentService { } return false; } + + /** + * Arrange for the UploaderService to be run on a regular basis. + * + * Any existing scheduled invocation of UploaderService is removed and optionally rescheduled. + * This may cause problems if this method is called so often that no scheduled invocation is + * ever run. But if the delay is short enough that it will go off when the user is sleeping, + * then there should be no starvation. + * + * @param context {@link Context} object + * @param needsRescheduling whether to schedule a future intent to be delivered to this service + */ + public static void cancelAndRescheduleUploadingService(final Context context, + final boolean needsRescheduling) { + final PendingIntent pendingIntent = getPendingIntentForService(context); + final AlarmManager alarmManager = (AlarmManager) context.getSystemService( + Context.ALARM_SERVICE); + cancelAnyScheduledServiceAlarm(alarmManager, pendingIntent); + if (needsRescheduling) { + scheduleServiceAlarm(alarmManager, pendingIntent); + } + } + + private static PendingIntent getPendingIntentForService(final Context context) { + final Intent intent = new Intent(context, UploaderService.class); + return PendingIntent.getService(context, 0, intent, 0); + } + + private static void cancelAnyScheduledServiceAlarm(final AlarmManager alarmManager, + final PendingIntent pendingIntent) { + alarmManager.cancel(pendingIntent); + } + + private static void scheduleServiceAlarm(final AlarmManager alarmManager, + final PendingIntent pendingIntent) { + alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, UploaderService.RUN_INTERVAL, + pendingIntent); + } } diff --git a/native/jni/com_android_inputmethod_keyboard_ProximityInfo.cpp b/native/jni/com_android_inputmethod_keyboard_ProximityInfo.cpp index 3c482ca58..dedb02abf 100644 --- a/native/jni/com_android_inputmethod_keyboard_ProximityInfo.cpp +++ b/native/jni/com_android_inputmethod_keyboard_ProximityInfo.cpp @@ -26,13 +26,13 @@ namespace latinime { static jlong latinime_Keyboard_setProximityInfo(JNIEnv *env, jclass clazz, jstring localeJStr, jint displayWidth, jint displayHeight, jint gridWidth, jint gridHeight, - jint mostCommonkeyWidth, jintArray proximityChars, jint keyCount, + jint mostCommonkeyWidth, jint mostCommonkeyHeight, jintArray proximityChars, jint keyCount, jintArray keyXCoordinates, jintArray keyYCoordinates, jintArray keyWidths, jintArray keyHeights, jintArray keyCharCodes, jfloatArray sweetSpotCenterXs, jfloatArray sweetSpotCenterYs, jfloatArray sweetSpotRadii) { ProximityInfo *proximityInfo = new ProximityInfo(env, localeJStr, displayWidth, displayHeight, - gridWidth, gridHeight, mostCommonkeyWidth, proximityChars, keyCount, - keyXCoordinates, keyYCoordinates, keyWidths, keyHeights, keyCharCodes, + gridWidth, gridHeight, mostCommonkeyWidth, mostCommonkeyHeight, proximityChars, + keyCount, keyXCoordinates, keyYCoordinates, keyWidths, keyHeights, keyCharCodes, sweetSpotCenterXs, sweetSpotCenterYs, sweetSpotRadii); return reinterpret_cast<jlong>(proximityInfo); } @@ -44,7 +44,7 @@ static void latinime_Keyboard_release(JNIEnv *env, jclass clazz, jlong proximity static JNINativeMethod sMethods[] = { {const_cast<char *>("setProximityInfoNative"), - const_cast<char *>("(Ljava/lang/String;IIIII[II[I[I[I[I[I[F[F[F)J"), + const_cast<char *>("(Ljava/lang/String;IIIIII[II[I[I[I[I[I[F[F[F)J"), reinterpret_cast<void *>(latinime_Keyboard_setProximityInfo)}, {const_cast<char *>("releaseProximityInfoNative"), const_cast<char *>("(J)V"), diff --git a/native/jni/src/defines.h b/native/jni/src/defines.h index a45691261..a7b023a75 100644 --- a/native/jni/src/defines.h +++ b/native/jni/src/defines.h @@ -216,6 +216,7 @@ static inline void prof_out(void) { #define DEBUG_DOUBLE_LETTER false #define DEBUG_CACHE false #define DEBUG_DUMP_ERROR false +#define DEBUG_EVALUATE_MOST_PROBABLE_STRING false #ifdef FLAG_FULL_DBG #define DEBUG_GEO_FULL true @@ -241,6 +242,7 @@ static inline void prof_out(void) { #define DEBUG_DOUBLE_LETTER false #define DEBUG_CACHE false #define DEBUG_DUMP_ERROR false +#define DEBUG_EVALUATE_MOST_PROBABLE_STRING false #define DEBUG_GEO_FULL false diff --git a/native/jni/src/dictionary.cpp b/native/jni/src/dictionary.cpp index ed6ddb517..c998c0676 100644 --- a/native/jni/src/dictionary.cpp +++ b/native/jni/src/dictionary.cpp @@ -103,4 +103,9 @@ int Dictionary::getProbability(const int *word, int length) const { bool Dictionary::isValidBigram(const int *word1, int length1, const int *word2, int length2) const { return mBigramDictionary->isValidBigram(word1, length1, word2, length2); } + +int Dictionary::getDictFlags() const { + return mUnigramDictionary->getDictFlags(); +} + } // namespace latinime diff --git a/native/jni/src/dictionary.h b/native/jni/src/dictionary.h index 8c6a7de52..0653d3ca9 100644 --- a/native/jni/src/dictionary.h +++ b/native/jni/src/dictionary.h @@ -63,6 +63,7 @@ class Dictionary { int getDictSize() const { return mDictSize; } int getMmapFd() const { return mMmapFd; } int getDictBufAdjust() const { return mDictBufAdjust; } + int getDictFlags() const; virtual ~Dictionary(); private: diff --git a/native/jni/src/digraph_utils.cpp b/native/jni/src/digraph_utils.cpp index 8781c5077..6a1ab0271 100644 --- a/native/jni/src/digraph_utils.cpp +++ b/native/jni/src/digraph_utils.cpp @@ -27,39 +27,47 @@ const DigraphUtils::digraph_t DigraphUtils::GERMAN_UMLAUT_DIGRAPHS[] = const DigraphUtils::digraph_t DigraphUtils::FRENCH_LIGATURES_DIGRAPHS[] = { { 'a', 'e', 0x00E6 }, // U+00E6 : LATIN SMALL LETTER AE { 'o', 'e', 0x0153 } }; // U+0153 : LATIN SMALL LIGATURE OE +const DigraphUtils::DigraphType DigraphUtils::USED_DIGRAPH_TYPES[] = + { DIGRAPH_TYPE_GERMAN_UMLAUT, DIGRAPH_TYPE_FRENCH_LIGATURES }; /* static */ bool DigraphUtils::hasDigraphForCodePoint( const int dictFlags, const int compositeGlyphCodePoint) { - if (DigraphUtils::getDigraphForCodePoint(dictFlags, compositeGlyphCodePoint)) { + const DigraphUtils::DigraphType digraphType = getDigraphTypeForDictionary(dictFlags); + if (DigraphUtils::getDigraphForDigraphTypeAndCodePoint(digraphType, compositeGlyphCodePoint)) { return true; } return false; } -// Retrieves the set of all digraphs associated with the given dictionary. -// Returns the size of the digraph array, or 0 if none exist. -/* static */ int DigraphUtils::getAllDigraphsForDictionaryAndReturnSize( - const int dictFlags, const DigraphUtils::digraph_t **digraphs) { +// Returns the digraph type associated with the given dictionary. +/* static */ DigraphUtils::DigraphType DigraphUtils::getDigraphTypeForDictionary( + const int dictFlags) { if (BinaryFormat::REQUIRES_GERMAN_UMLAUT_PROCESSING & dictFlags) { - *digraphs = DigraphUtils::GERMAN_UMLAUT_DIGRAPHS; - return NELEMS(DigraphUtils::GERMAN_UMLAUT_DIGRAPHS); + return DIGRAPH_TYPE_GERMAN_UMLAUT; } if (BinaryFormat::REQUIRES_FRENCH_LIGATURES_PROCESSING & dictFlags) { - *digraphs = DigraphUtils::FRENCH_LIGATURES_DIGRAPHS; - return NELEMS(DigraphUtils::FRENCH_LIGATURES_DIGRAPHS); + return DIGRAPH_TYPE_FRENCH_LIGATURES; } - return 0; + return DIGRAPH_TYPE_NONE; +} + +// Retrieves the set of all digraphs associated with the given dictionary flags. +// Returns the size of the digraph array, or 0 if none exist. +/* static */ int DigraphUtils::getAllDigraphsForDictionaryAndReturnSize( + const int dictFlags, const DigraphUtils::digraph_t **const digraphs) { + const DigraphUtils::DigraphType digraphType = getDigraphTypeForDictionary(dictFlags); + return getAllDigraphsForDigraphTypeAndReturnSize(digraphType, digraphs); } // Returns the digraph codepoint for the given composite glyph codepoint and digraph codepoint index // (which specifies the first or second codepoint in the digraph). -/* static */ int DigraphUtils::getDigraphCodePointForIndex(const int dictFlags, - const int compositeGlyphCodePoint, const DigraphCodePointIndex digraphCodePointIndex) { +/* static */ int DigraphUtils::getDigraphCodePointForIndex(const int compositeGlyphCodePoint, + const DigraphCodePointIndex digraphCodePointIndex) { if (digraphCodePointIndex == NOT_A_DIGRAPH_INDEX) { return NOT_A_CODE_POINT; } - const DigraphUtils::digraph_t *digraph = - DigraphUtils::getDigraphForCodePoint(dictFlags, compositeGlyphCodePoint); + const DigraphUtils::digraph_t *const digraph = + DigraphUtils::getDigraphForCodePoint(compositeGlyphCodePoint); if (!digraph) { return NOT_A_CODE_POINT; } @@ -72,16 +80,48 @@ const DigraphUtils::digraph_t DigraphUtils::FRENCH_LIGATURES_DIGRAPHS[] = return NOT_A_CODE_POINT; } +// Retrieves the set of all digraphs associated with the given digraph type. +// Returns the size of the digraph array, or 0 if none exist. +/* static */ int DigraphUtils::getAllDigraphsForDigraphTypeAndReturnSize( + const DigraphUtils::DigraphType digraphType, + const DigraphUtils::digraph_t **const digraphs) { + if (digraphType == DigraphUtils::DIGRAPH_TYPE_GERMAN_UMLAUT) { + *digraphs = GERMAN_UMLAUT_DIGRAPHS; + return NELEMS(GERMAN_UMLAUT_DIGRAPHS); + } + if (digraphType == DIGRAPH_TYPE_FRENCH_LIGATURES) { + *digraphs = FRENCH_LIGATURES_DIGRAPHS; + return NELEMS(FRENCH_LIGATURES_DIGRAPHS); + } + return 0; +} + /** * Returns the digraph for the input composite glyph codepoint, or 0 if none exists. - * dictFlags: the dictionary flags needed to determine which digraphs are supported. * compositeGlyphCodePoint: the method returns the digraph corresponding to this codepoint. */ /* static */ const DigraphUtils::digraph_t *DigraphUtils::getDigraphForCodePoint( - const int dictFlags, const int compositeGlyphCodePoint) { + const int compositeGlyphCodePoint) { + for (size_t i = 0; i < NELEMS(USED_DIGRAPH_TYPES); i++) { + const DigraphUtils::digraph_t *const digraph = getDigraphForDigraphTypeAndCodePoint( + USED_DIGRAPH_TYPES[i], compositeGlyphCodePoint); + if (digraph) { + return digraph; + } + } + return 0; +} + +/** + * Returns the digraph for the input composite glyph codepoint, or 0 if none exists. + * digraphType: the type of digraphs supported. + * compositeGlyphCodePoint: the method returns the digraph corresponding to this codepoint. + */ +/* static */ const DigraphUtils::digraph_t *DigraphUtils::getDigraphForDigraphTypeAndCodePoint( + const DigraphUtils::DigraphType digraphType, const int compositeGlyphCodePoint) { const DigraphUtils::digraph_t *digraphs = 0; const int digraphsSize = - DigraphUtils::getAllDigraphsForDictionaryAndReturnSize(dictFlags, &digraphs); + DigraphUtils::getAllDigraphsForDictionaryAndReturnSize(digraphType, &digraphs); for (int i = 0; i < digraphsSize; i++) { if (digraphs[i].compositeGlyph == compositeGlyphCodePoint) { return &digraphs[i]; diff --git a/native/jni/src/digraph_utils.h b/native/jni/src/digraph_utils.h index 6e364b67a..94435228e 100644 --- a/native/jni/src/digraph_utils.h +++ b/native/jni/src/digraph_utils.h @@ -27,21 +27,34 @@ class DigraphUtils { SECOND_DIGRAPH_CODEPOINT } DigraphCodePointIndex; + typedef enum { + DIGRAPH_TYPE_NONE, + DIGRAPH_TYPE_GERMAN_UMLAUT, + DIGRAPH_TYPE_FRENCH_LIGATURES + } DigraphType; + typedef struct { int first; int second; int compositeGlyph; } digraph_t; static bool hasDigraphForCodePoint(const int dictFlags, const int compositeGlyphCodePoint); static int getAllDigraphsForDictionaryAndReturnSize( - const int dictFlags, const digraph_t **digraphs); + const int dictFlags, const digraph_t **const digraphs); static int getDigraphCodePointForIndex(const int dictFlags, const int compositeGlyphCodePoint, const DigraphCodePointIndex digraphCodePointIndex); + static int getDigraphCodePointForIndex(const int compositeGlyphCodePoint, + const DigraphCodePointIndex digraphCodePointIndex); private: DISALLOW_IMPLICIT_CONSTRUCTORS(DigraphUtils); - static const digraph_t *getDigraphForCodePoint( - const int dictFlags, const int compositeGlyphCodePoint); + static DigraphType getDigraphTypeForDictionary(const int dictFlags); + static int getAllDigraphsForDigraphTypeAndReturnSize( + const DigraphType digraphType, const digraph_t **const digraphs); + static const digraph_t *getDigraphForCodePoint(const int compositeGlyphCodePoint); + static const digraph_t *getDigraphForDigraphTypeAndCodePoint( + const DigraphType digraphType, const int compositeGlyphCodePoint); static const digraph_t GERMAN_UMLAUT_DIGRAPHS[]; static const digraph_t FRENCH_LIGATURES_DIGRAPHS[]; + static const DigraphType USED_DIGRAPH_TYPES[]; }; } // namespace latinime #endif // DIGRAPH_UTILS_H diff --git a/native/jni/src/proximity_info.cpp b/native/jni/src/proximity_info.cpp index 50f38e82e..88d670d61 100644 --- a/native/jni/src/proximity_info.cpp +++ b/native/jni/src/proximity_info.cpp @@ -49,13 +49,17 @@ static AK_FORCE_INLINE void safeGetOrFillZeroFloatArrayRegion(JNIEnv *env, jfloa ProximityInfo::ProximityInfo(JNIEnv *env, const jstring localeJStr, const int keyboardWidth, const int keyboardHeight, const int gridWidth, - const int gridHeight, const int mostCommonKeyWidth, const jintArray proximityChars, - const int keyCount, const jintArray keyXCoordinates, const jintArray keyYCoordinates, - const jintArray keyWidths, const jintArray keyHeights, const jintArray keyCharCodes, - const jfloatArray sweetSpotCenterXs, const jfloatArray sweetSpotCenterYs, - const jfloatArray sweetSpotRadii) + const int gridHeight, const int mostCommonKeyWidth, const int mostCommonKeyHeight, + const jintArray proximityChars, const int keyCount, const jintArray keyXCoordinates, + const jintArray keyYCoordinates, const jintArray keyWidths, const jintArray keyHeights, + const jintArray keyCharCodes, const jfloatArray sweetSpotCenterXs, + const jfloatArray sweetSpotCenterYs, const jfloatArray sweetSpotRadii) : GRID_WIDTH(gridWidth), GRID_HEIGHT(gridHeight), MOST_COMMON_KEY_WIDTH(mostCommonKeyWidth), MOST_COMMON_KEY_WIDTH_SQUARE(mostCommonKeyWidth * mostCommonKeyWidth), + MOST_COMMON_KEY_HEIGHT(mostCommonKeyHeight), + NORMALIZED_SQUARED_MOST_COMMON_KEY_HYPOTENUSE(1.0f + + SQUARE_FLOAT(static_cast<float>(mostCommonKeyHeight) / + static_cast<float>(mostCommonKeyWidth))), CELL_WIDTH((keyboardWidth + gridWidth - 1) / gridWidth), CELL_HEIGHT((keyboardHeight + gridHeight - 1) / gridHeight), KEY_COUNT(min(keyCount, MAX_KEY_COUNT_IN_A_KEYBOARD)), diff --git a/native/jni/src/proximity_info.h b/native/jni/src/proximity_info.h index e21262fdb..deb9ae0de 100644 --- a/native/jni/src/proximity_info.h +++ b/native/jni/src/proximity_info.h @@ -30,11 +30,11 @@ class ProximityInfo { public: ProximityInfo(JNIEnv *env, const jstring localeJStr, const int keyboardWidth, const int keyboardHeight, const int gridWidth, - const int gridHeight, const int mostCommonKeyWidth, const jintArray proximityChars, - const int keyCount, const jintArray keyXCoordinates, const jintArray keyYCoordinates, - const jintArray keyWidths, const jintArray keyHeights, const jintArray keyCharCodes, - const jfloatArray sweetSpotCenterXs, const jfloatArray sweetSpotCenterYs, - const jfloatArray sweetSpotRadii); + const int gridHeight, const int mostCommonKeyWidth, const int mostCommonKeyHeight, + const jintArray proximityChars, const int keyCount, const jintArray keyXCoordinates, + const jintArray keyYCoordinates, const jintArray keyWidths, const jintArray keyHeights, + const jintArray keyCharCodes, const jfloatArray sweetSpotCenterXs, + const jfloatArray sweetSpotCenterYs, const jfloatArray sweetSpotRadii); ~ProximityInfo(); bool hasSpaceProximity(const int x, const int y) const; int getNormalizedSquaredDistance(const int inputIndex, const int proximityIndex) const; @@ -56,6 +56,9 @@ class ProximityInfo { bool hasTouchPositionCorrectionData() const { return HAS_TOUCH_POSITION_CORRECTION_DATA; } int getMostCommonKeyWidth() const { return MOST_COMMON_KEY_WIDTH; } int getMostCommonKeyWidthSquare() const { return MOST_COMMON_KEY_WIDTH_SQUARE; } + float getNormalizedSquaredMostCommonKeyHypotenuse() const { + return NORMALIZED_SQUARED_MOST_COMMON_KEY_HYPOTENUSE; + } int getKeyCount() const { return KEY_COUNT; } int getCellHeight() const { return CELL_HEIGHT; } int getCellWidth() const { return CELL_WIDTH; } @@ -99,6 +102,8 @@ class ProximityInfo { const int GRID_HEIGHT; const int MOST_COMMON_KEY_WIDTH; const int MOST_COMMON_KEY_WIDTH_SQUARE; + const int MOST_COMMON_KEY_HEIGHT; + const float NORMALIZED_SQUARED_MOST_COMMON_KEY_HYPOTENUSE; const int CELL_WIDTH; const int CELL_HEIGHT; const int KEY_COUNT; diff --git a/native/jni/src/suggest/core/dicnode/dic_node.h b/native/jni/src/suggest/core/dicnode/dic_node.h index cde7b99a7..32faae52c 100644 --- a/native/jni/src/suggest/core/dicnode/dic_node.h +++ b/native/jni/src/suggest/core/dicnode/dic_node.h @@ -23,6 +23,7 @@ #include "dic_node_profiler.h" #include "dic_node_properties.h" #include "dic_node_release_listener.h" +#include "digraph_utils.h" #if DEBUG_DICT #define LOGI_SHOW_ADD_COST_PROP \ @@ -399,8 +400,15 @@ class DicNode { // TODO: Remove // ////////////////////// // TODO: Remove once touch path is merged into ProximityInfoState + // Note: Returned codepoint may be a digraph codepoint if the node is in a composite glyph. int getNodeCodePoint() const { - return mDicNodeProperties.getNodeCodePoint(); + const int codePoint = mDicNodeProperties.getNodeCodePoint(); + const DigraphUtils::DigraphCodePointIndex digraphIndex = + mDicNodeState.mDicNodeStateScoring.getDigraphIndex(); + if (digraphIndex == DigraphUtils::NOT_A_DIGRAPH_INDEX) { + return codePoint; + } + return DigraphUtils::getDigraphCodePointForIndex(codePoint, digraphIndex); } //////////////////////////////// @@ -452,6 +460,15 @@ class DicNode { mDicNodeState.mDicNodeStateScoring.setDoubleLetterLevel(doubleLetterLevel); } + bool isInDigraph() const { + return mDicNodeState.mDicNodeStateScoring.getDigraphIndex() + != DigraphUtils::NOT_A_DIGRAPH_INDEX; + } + + void advanceDigraphIndex() { + mDicNodeState.mDicNodeStateScoring.advanceDigraphIndex(); + } + uint8_t getFlags() const { return mDicNodeProperties.getFlags(); } diff --git a/native/jni/src/suggest/core/dicnode/dic_node_state_scoring.h b/native/jni/src/suggest/core/dicnode/dic_node_state_scoring.h index 8e816329f..8902d3122 100644 --- a/native/jni/src/suggest/core/dicnode/dic_node_state_scoring.h +++ b/native/jni/src/suggest/core/dicnode/dic_node_state_scoring.h @@ -20,6 +20,7 @@ #include <stdint.h> #include "defines.h" +#include "digraph_utils.h" namespace latinime { @@ -27,6 +28,7 @@ class DicNodeStateScoring { public: AK_FORCE_INLINE DicNodeStateScoring() : mDoubleLetterLevel(NOT_A_DOUBLE_LETTER), + mDigraphIndex(DigraphUtils::NOT_A_DIGRAPH_INDEX), mEditCorrectionCount(0), mProximityCorrectionCount(0), mNormalizedCompoundDistance(0.0f), mSpatialDistance(0.0f), mLanguageDistance(0.0f), mTotalPrevWordsLanguageCost(0.0f), mRawLength(0.0f) { @@ -43,6 +45,7 @@ class DicNodeStateScoring { mTotalPrevWordsLanguageCost = 0.0f; mRawLength = 0.0f; mDoubleLetterLevel = NOT_A_DOUBLE_LETTER; + mDigraphIndex = DigraphUtils::NOT_A_DIGRAPH_INDEX; } AK_FORCE_INLINE void init(const DicNodeStateScoring *const scoring) { @@ -54,6 +57,7 @@ class DicNodeStateScoring { mTotalPrevWordsLanguageCost = scoring->mTotalPrevWordsLanguageCost; mRawLength = scoring->mRawLength; mDoubleLetterLevel = scoring->mDoubleLetterLevel; + mDigraphIndex = scoring->mDigraphIndex; } void addCost(const float spatialCost, const float languageCost, const bool doNormalization, @@ -126,6 +130,24 @@ class DicNodeStateScoring { } } + DigraphUtils::DigraphCodePointIndex getDigraphIndex() const { + return mDigraphIndex; + } + + void advanceDigraphIndex() { + switch(mDigraphIndex) { + case DigraphUtils::NOT_A_DIGRAPH_INDEX: + mDigraphIndex = DigraphUtils::FIRST_DIGRAPH_CODEPOINT; + break; + case DigraphUtils::FIRST_DIGRAPH_CODEPOINT: + mDigraphIndex = DigraphUtils::SECOND_DIGRAPH_CODEPOINT; + break; + case DigraphUtils::SECOND_DIGRAPH_CODEPOINT: + mDigraphIndex = DigraphUtils::NOT_A_DIGRAPH_INDEX; + break; + } + } + float getTotalPrevWordsLanguageCost() const { return mTotalPrevWordsLanguageCost; } @@ -135,6 +157,7 @@ class DicNodeStateScoring { // Use a default copy constructor and an assign operator because shallow copies are ok // for this class DoubleLetterLevel mDoubleLetterLevel; + DigraphUtils::DigraphCodePointIndex mDigraphIndex; int16_t mEditCorrectionCount; int16_t mProximityCorrectionCount; diff --git a/native/jni/src/suggest/core/session/dic_traverse_session.cpp b/native/jni/src/suggest/core/session/dic_traverse_session.cpp index ef6616e40..5b783a2ba 100644 --- a/native/jni/src/suggest/core/session/dic_traverse_session.cpp +++ b/native/jni/src/suggest/core/session/dic_traverse_session.cpp @@ -84,6 +84,10 @@ const uint8_t *DicTraverseSession::getOffsetDict() const { return mDictionary->getOffsetDict(); } +int DicTraverseSession::getDictFlags() const { + return mDictionary->getDictFlags(); +} + void DicTraverseSession::resetCache(const int nextActiveCacheSize, const int maxWords) { mDicNodesCache.reset(nextActiveCacheSize, maxWords); mBigramCacheMap.clear(); diff --git a/native/jni/src/suggest/core/session/dic_traverse_session.h b/native/jni/src/suggest/core/session/dic_traverse_session.h index 62e1d1ab9..525d198cd 100644 --- a/native/jni/src/suggest/core/session/dic_traverse_session.h +++ b/native/jni/src/suggest/core/session/dic_traverse_session.h @@ -53,7 +53,7 @@ class DicTraverseSession { void resetCache(const int nextActiveCacheSize, const int maxWords); const uint8_t *getOffsetDict() const; - bool canUseCache() const; + int getDictFlags() const; //-------------------- // getters and setters diff --git a/native/jni/src/suggest/core/suggest.cpp b/native/jni/src/suggest/core/suggest.cpp index 764c37240..67d351fa1 100644 --- a/native/jni/src/suggest/core/suggest.cpp +++ b/native/jni/src/suggest/core/suggest.cpp @@ -18,6 +18,7 @@ #include "char_utils.h" #include "dictionary.h" +#include "digraph_utils.h" #include "proximity_info.h" #include "suggest/core/dicnode/dic_node.h" #include "suggest/core/dicnode/dic_node_priority_queue.h" @@ -123,8 +124,12 @@ void Suggest::initializeSearch(DicTraverseSession *traverseSession, int commitPo */ int Suggest::outputSuggestions(DicTraverseSession *traverseSession, int *frequencies, int *outputCodePoints, int *spaceIndices, int *outputTypes) const { +#if DEBUG_EVALUATE_MOST_PROBABLE_STRING + const int terminalSize = 0; +#else const int terminalSize = min(MAX_RESULTS, static_cast<int>(traverseSession->getDicTraverseCache()->terminalSize())); +#endif DicNode terminals[MAX_RESULTS]; // Avoiding non-POD variable length array for (int index = terminalSize - 1; index >= 0; --index) { @@ -221,7 +226,7 @@ int Suggest::outputSuggestions(DicTraverseSession *traverseSession, int *frequen void Suggest::expandCurrentDicNodes(DicTraverseSession *traverseSession) const { const int inputSize = traverseSession->getInputSize(); DicNodeVector childDicNodes(TRAVERSAL->getDefaultExpandDicNodeSize()); - DicNode omissionDicNode; + DicNode correctionDicNode; // TODO: Find more efficient caching const bool shouldDepthLevelCache = TRAVERSAL->shouldDepthLevelCache(traverseSession); @@ -257,7 +262,10 @@ void Suggest::expandCurrentDicNodes(DicTraverseSession *traverseSession) const { dicNode.setCached(); } - if (isLookAheadCorrection) { + if (dicNode.isInDigraph()) { + // Finish digraph handling if the node is in the middle of a digraph expansion. + processDicNodeAsDigraph(traverseSession, &dicNode); + } else if (isLookAheadCorrection) { // The algorithm maintains a small set of "deferred" nodes that have not consumed the // latest touch point yet. These are needed to apply look-ahead correction operations // that require special handling of the latest touch point. For example, with insertions @@ -291,12 +299,18 @@ void Suggest::expandCurrentDicNodes(DicTraverseSession *traverseSession) const { processDicNodeAsMatch(traverseSession, childDicNode); continue; } + if (DigraphUtils::hasDigraphForCodePoint(traverseSession->getDictFlags(), + childDicNode->getNodeCodePoint())) { + correctionDicNode.initByCopy(childDicNode); + correctionDicNode.advanceDigraphIndex(); + processDicNodeAsDigraph(traverseSession, &correctionDicNode); + } if (allowsErrorCorrections && TRAVERSAL->isOmission(traverseSession, &dicNode, childDicNode)) { // TODO: (Gesture) Change weight between omission and substitution errors // TODO: (Gesture) Terminal node should not be handled as omission - omissionDicNode.initByCopy(childDicNode); - processDicNodeAsOmission(traverseSession, &omissionDicNode); + correctionDicNode.initByCopy(childDicNode); + processDicNodeAsOmission(traverseSession, &correctionDicNode); } const ProximityType proximityType = TRAVERSAL->getProximityType( traverseSession, &dicNode, childDicNode); @@ -400,6 +414,16 @@ void Suggest::processDicNodeAsSubstitution(DicTraverseSession *traverseSession, processExpandedDicNode(traverseSession, childDicNode); } +// Process the node codepoint as a digraph. This means that composite glyphs like the German +// u-umlaut is expanded to the transliteration "ue". Note that this happens in parallel with +// the normal non-digraph traversal, so both "uber" and "ueber" can be corrected to "[u-umlaut]ber". +void Suggest::processDicNodeAsDigraph(DicTraverseSession *traverseSession, + DicNode *childDicNode) const { + weightChildNode(traverseSession, childDicNode); + childDicNode->advanceDigraphIndex(); + processExpandedDicNode(traverseSession, childDicNode); +} + /** * Handle the dicNode as an omission error (e.g., ths => this). Skip the current letter and consider * matches for all possible next letters. Note that just skipping the current letter without any diff --git a/native/jni/src/suggest/core/suggest.h b/native/jni/src/suggest/core/suggest.h index 9f609c50c..becd6c1de 100644 --- a/native/jni/src/suggest/core/suggest.h +++ b/native/jni/src/suggest/core/suggest.h @@ -65,6 +65,7 @@ class Suggest : public SuggestInterface { void generateFeatures( DicTraverseSession *traverseSession, DicNode *dicNode, float *features) const; void processDicNodeAsOmission(DicTraverseSession *traverseSession, DicNode *dicNode) const; + void processDicNodeAsDigraph(DicTraverseSession *traverseSession, DicNode *dicNode) const; void processDicNodeAsTransposition(DicTraverseSession *traverseSession, DicNode *dicNode) const; void processDicNodeAsInsertion(DicTraverseSession *traverseSession, DicNode *dicNode) const; diff --git a/native/jni/src/unigram_dictionary.cpp b/native/jni/src/unigram_dictionary.cpp index 33795cade..a672294b5 100644 --- a/native/jni/src/unigram_dictionary.cpp +++ b/native/jni/src/unigram_dictionary.cpp @@ -32,9 +32,9 @@ namespace latinime { // TODO: check the header -UnigramDictionary::UnigramDictionary(const uint8_t *const streamStart, const unsigned int flags) +UnigramDictionary::UnigramDictionary(const uint8_t *const streamStart, const unsigned int dictFlags) : DICT_ROOT(streamStart), ROOT_POS(0), - MAX_DIGRAPH_SEARCH_DEPTH(DEFAULT_MAX_DIGRAPH_SEARCH_DEPTH), FLAGS(flags) { + MAX_DIGRAPH_SEARCH_DEPTH(DEFAULT_MAX_DIGRAPH_SEARCH_DEPTH), DICT_FLAGS(dictFlags) { if (DEBUG_DICT) { AKLOGI("UnigramDictionary - constructor"); } @@ -163,7 +163,7 @@ int UnigramDictionary::getSuggestions(ProximityInfo *proximityInfo, const int *x masterCorrection.resetCorrection(); const DigraphUtils::digraph_t *digraphs = 0; const int digraphsSize = - DigraphUtils::getAllDigraphsForDictionaryAndReturnSize(FLAGS, &digraphs); + DigraphUtils::getAllDigraphsForDictionaryAndReturnSize(DICT_FLAGS, &digraphs); if (digraphsSize > 0) { // Incrementally tune the word and try all possibilities int codesBuffer[sizeof(*inputCodePoints) * inputSize]; diff --git a/native/jni/src/unigram_dictionary.h b/native/jni/src/unigram_dictionary.h index 1a01758fe..a64a539bd 100644 --- a/native/jni/src/unigram_dictionary.h +++ b/native/jni/src/unigram_dictionary.h @@ -38,7 +38,7 @@ class UnigramDictionary { static const int FLAG_MULTIPLE_SUGGEST_ABORT = 0; static const int FLAG_MULTIPLE_SUGGEST_SKIP = 1; static const int FLAG_MULTIPLE_SUGGEST_CONTINUE = 2; - UnigramDictionary(const uint8_t *const streamStart, const unsigned int flags); + UnigramDictionary(const uint8_t *const streamStart, const unsigned int dictFlags); int getProbability(const int *const inWord, const int length) const; int getBigramPosition(int pos, int *word, int offset, int length) const; int getSuggestions(ProximityInfo *proximityInfo, const int *xcoordinates, @@ -46,6 +46,7 @@ class UnigramDictionary { const std::map<int, int> *bigramMap, const uint8_t *bigramFilter, const bool useFullEditDistance, int *outWords, int *frequencies, int *outputTypes) const; + int getDictFlags() const { return DICT_FLAGS; } virtual ~UnigramDictionary(); private: @@ -109,7 +110,7 @@ class UnigramDictionary { const uint8_t *const DICT_ROOT; const int ROOT_POS; const int MAX_DIGRAPH_SEARCH_DEPTH; - const int FLAGS; + const int DICT_FLAGS; }; } // namespace latinime #endif // LATINIME_UNIGRAM_DICTIONARY_H diff --git a/tests/src/com/android/inputmethod/keyboard/SpacebarTextTests.java b/tests/src/com/android/inputmethod/keyboard/SpacebarTextTests.java index 1398db97c..850af94f7 100644 --- a/tests/src/com/android/inputmethod/keyboard/SpacebarTextTests.java +++ b/tests/src/com/android/inputmethod/keyboard/SpacebarTextTests.java @@ -113,7 +113,8 @@ public class SpacebarTextTests extends AndroidTestCase { final String subtypeName = SubtypeLocale.getSubtypeDisplayName(subtype); final Locale locale = SubtypeLocale.getSubtypeLocale(subtype); final String spacebarText = MainKeyboardView.getShortDisplayName(subtype); - final String languageCode = StringUtils.toTitleCase(locale.getLanguage(), locale); + final String languageCode = StringUtils.capitalizeFirstCodePoint( + locale.getLanguage(), locale); if (SubtypeLocale.isNoLanguage(subtype)) { assertEquals(subtypeName, "", spacebarText); } else { diff --git a/tests/src/com/android/inputmethod/latin/StringUtilsTests.java b/tests/src/com/android/inputmethod/latin/StringUtilsTests.java index 966919ed3..136faff71 100644 --- a/tests/src/com/android/inputmethod/latin/StringUtilsTests.java +++ b/tests/src/com/android/inputmethod/latin/StringUtilsTests.java @@ -93,25 +93,43 @@ public class StringUtilsTests extends AndroidTestCase { StringUtils.removeFromCsvIfExists("key", "key1,key,key3,key,key5")); } - public void testToTitleCase() { + + public void testCapitalizeFirstCodePoint() { + assertEquals("SSaa", + StringUtils.capitalizeFirstCodePoint("ßaa", Locale.GERMAN)); + assertEquals("Aßa", + StringUtils.capitalizeFirstCodePoint("aßa", Locale.GERMAN)); + assertEquals("Iab", + StringUtils.capitalizeFirstCodePoint("iab", Locale.ENGLISH)); + assertEquals("CAmElCaSe", + StringUtils.capitalizeFirstCodePoint("cAmElCaSe", Locale.ENGLISH)); + assertEquals("İab", + StringUtils.capitalizeFirstCodePoint("iab", new Locale("tr"))); + assertEquals("AİB", + StringUtils.capitalizeFirstCodePoint("AİB", new Locale("tr"))); + assertEquals("A", + StringUtils.capitalizeFirstCodePoint("a", Locale.ENGLISH)); + assertEquals("A", + StringUtils.capitalizeFirstCodePoint("A", Locale.ENGLISH)); + } + + public void testCapitalizeFirstAndDowncaseRest() { assertEquals("SSaa", - StringUtils.toTitleCase("ßaa", Locale.GERMAN)); + StringUtils.capitalizeFirstAndDowncaseRest("ßaa", Locale.GERMAN)); assertEquals("Aßa", - StringUtils.toTitleCase("aßa", Locale.GERMAN)); + StringUtils.capitalizeFirstAndDowncaseRest("aßa", Locale.GERMAN)); assertEquals("Iab", - StringUtils.toTitleCase("iab", Locale.ENGLISH)); + StringUtils.capitalizeFirstAndDowncaseRest("iab", Locale.ENGLISH)); assertEquals("Camelcase", - StringUtils.toTitleCase("cAmElCaSe", Locale.ENGLISH)); + StringUtils.capitalizeFirstAndDowncaseRest("cAmElCaSe", Locale.ENGLISH)); assertEquals("İab", - StringUtils.toTitleCase("iab", new Locale("tr"))); + StringUtils.capitalizeFirstAndDowncaseRest("iab", new Locale("tr"))); assertEquals("Aib", - StringUtils.toTitleCase("AİB", new Locale("tr"))); - // For one character, toTitleCase returns the string as is. Not sure what the motivation - // is, but that's how it works now. - assertEquals("a", - StringUtils.toTitleCase("a", Locale.ENGLISH)); + StringUtils.capitalizeFirstAndDowncaseRest("AİB", new Locale("tr"))); assertEquals("A", - StringUtils.toTitleCase("A", Locale.ENGLISH)); + StringUtils.capitalizeFirstAndDowncaseRest("a", Locale.ENGLISH)); + assertEquals("A", + StringUtils.capitalizeFirstAndDowncaseRest("A", Locale.ENGLISH)); } public void testGetCapitalizationType() { @@ -136,4 +154,83 @@ public class StringUtilsTests extends AndroidTestCase { assertEquals(StringUtils.CAPITALIZE_NONE, StringUtils.getCapitalizationType("")); } + + public void testIsIdenticalAfterUpcaseIsIdenticalAfterDowncase() { + assertFalse(StringUtils.isIdenticalAfterUpcase("capitalize")); + assertTrue(StringUtils.isIdenticalAfterDowncase("capitalize")); + assertFalse(StringUtils.isIdenticalAfterUpcase("cApITalize")); + assertFalse(StringUtils.isIdenticalAfterDowncase("cApITalize")); + assertFalse(StringUtils.isIdenticalAfterUpcase("capitalizE")); + assertFalse(StringUtils.isIdenticalAfterDowncase("capitalizE")); + assertFalse(StringUtils.isIdenticalAfterUpcase("__c a piu$@tali56ze")); + assertTrue(StringUtils.isIdenticalAfterDowncase("__c a piu$@tali56ze")); + assertFalse(StringUtils.isIdenticalAfterUpcase("A__c a piu$@tali56ze")); + assertFalse(StringUtils.isIdenticalAfterDowncase("A__c a piu$@tali56ze")); + assertFalse(StringUtils.isIdenticalAfterUpcase("Capitalize")); + assertFalse(StringUtils.isIdenticalAfterDowncase("Capitalize")); + assertFalse(StringUtils.isIdenticalAfterUpcase(" Capitalize")); + assertFalse(StringUtils.isIdenticalAfterDowncase(" Capitalize")); + assertTrue(StringUtils.isIdenticalAfterUpcase("CAPITALIZE")); + assertFalse(StringUtils.isIdenticalAfterDowncase("CAPITALIZE")); + assertTrue(StringUtils.isIdenticalAfterUpcase(" PI26LIE")); + assertFalse(StringUtils.isIdenticalAfterDowncase(" PI26LIE")); + assertTrue(StringUtils.isIdenticalAfterUpcase("")); + assertTrue(StringUtils.isIdenticalAfterDowncase("")); + } + + private void checkCapitalize(final String src, final String dst, final String separators, + final Locale locale) { + assertEquals(dst, StringUtils.capitalizeEachWord(src, separators, locale)); + assert(src.equals(dst) + == StringUtils.isIdenticalAfterCapitalizeEachWord(src, separators)); + } + + public void testCapitalizeEachWord() { + checkCapitalize("", "", " ", Locale.ENGLISH); + checkCapitalize("test", "Test", " ", Locale.ENGLISH); + checkCapitalize(" test", " Test", " ", Locale.ENGLISH); + checkCapitalize("Test", "Test", " ", Locale.ENGLISH); + checkCapitalize(" Test", " Test", " ", Locale.ENGLISH); + checkCapitalize(".Test", ".test", " ", Locale.ENGLISH); + checkCapitalize(".Test", ".Test", " .", Locale.ENGLISH); + checkCapitalize(".Test", ".Test", ". ", Locale.ENGLISH); + checkCapitalize("test and retest", "Test And Retest", " .", Locale.ENGLISH); + checkCapitalize("Test and retest", "Test And Retest", " .", Locale.ENGLISH); + checkCapitalize("Test And Retest", "Test And Retest", " .", Locale.ENGLISH); + checkCapitalize("Test And.Retest ", "Test And.Retest ", " .", Locale.ENGLISH); + checkCapitalize("Test And.retest ", "Test And.Retest ", " .", Locale.ENGLISH); + checkCapitalize("Test And.retest ", "Test And.retest ", " ", Locale.ENGLISH); + checkCapitalize("Test And.Retest ", "Test And.retest ", " ", Locale.ENGLISH); + checkCapitalize("test and ietest", "Test And İetest", " .", new Locale("tr")); + checkCapitalize("test and ietest", "Test And Ietest", " .", Locale.ENGLISH); + checkCapitalize("Test&Retest", "Test&Retest", " \n.!?*()&", Locale.ENGLISH); + checkCapitalize("Test&retest", "Test&Retest", " \n.!?*()&", Locale.ENGLISH); + checkCapitalize("test&Retest", "Test&Retest", " \n.!?*()&", Locale.ENGLISH); + checkCapitalize("rest\nrecreation! And in the end...", + "Rest\nRecreation! And In The End...", " \n.!?*,();&", Locale.ENGLISH); + checkCapitalize("lorem ipsum dolor sit amet", "Lorem Ipsum Dolor Sit Amet", + " \n.,!?*()&;", Locale.ENGLISH); + checkCapitalize("Lorem!Ipsum (Dolor) Sit * Amet", "Lorem!Ipsum (Dolor) Sit * Amet", + " \n,.;!?*()&", Locale.ENGLISH); + checkCapitalize("Lorem!Ipsum (dolor) Sit * Amet", "Lorem!Ipsum (Dolor) Sit * Amet", + " \n,.;!?*()&", Locale.ENGLISH); + } + + public void testContainsAny() { + assertFalse(StringUtils.containsAny("", " ")); + assertFalse(StringUtils.containsAny("test and retest", "")); + assertTrue(StringUtils.containsAny("test and retest", "x3iq o")); + assertTrue(StringUtils.containsAny("test and retest", "x3iqo ")); + assertTrue(StringUtils.containsAny("test and retest", " x3iqo")); + assertFalse(StringUtils.containsAny("test and retest", "x3iqo")); + assertTrue(StringUtils.containsAny("test and retest", "tse ")); + assertTrue(StringUtils.containsAny("test and retest.", ".?()")); + assertFalse(StringUtils.containsAny("test and retest", ".?()")); + // Surrogate pair + assertTrue(StringUtils.containsAny("test and \uD861\uDED7 retest.", "\uD861\uDED7")); + // Ill-formed string + assertFalse(StringUtils.containsAny("test and \uD861 retest.", "\uD861\uDED7")); + // Ill-formed string + assertFalse(StringUtils.containsAny("test and \uDED7 retest.", "\uD861\uDED7")); + } } |