diff options
20 files changed, 582 insertions, 241 deletions
diff --git a/java/res/values-en/additional-proximitychars.xml b/java/res/values-en/additional-proximitychars.xml new file mode 100644 index 000000000..0e1276796 --- /dev/null +++ b/java/res/values-en/additional-proximitychars.xml @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* +** +** Copyright 2012, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ +--> +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + + <string-array name="additional_proximitychars"> + <!-- Empty entry terminates the proximity chars array. --> + + <!-- Additional proximity chars for a --> + <item>a</item> + <item>e</item> + <item>i</item> + <item>o</item> + <item>u</item> + <item></item> + <!-- Additional proximity chars for e --> + <item>e</item> + <item>a</item> + <item>i</item> + <item>o</item> + <item>u</item> + <item></item> + <!-- Additional proximity chars for i --> + <item>i</item> + <item>a</item> + <item>e</item> + <item>o</item> + <item>u</item> + <item></item> + <!-- Additional proximity chars for o --> + <item>o</item> + <item>a</item> + <item>e</item> + <item>i</item> + <item>u</item> + <item></item> + <!-- Additional proximity chars for u --> + <item>u</item> + <item>a</item> + <item>e</item> + <item>i</item> + <item>o</item> + <item></item> + </string-array> + +</resources>
\ No newline at end of file diff --git a/java/res/values/additional-proximitychars.xml b/java/res/values/additional-proximitychars.xml new file mode 100644 index 000000000..03d10d5d8 --- /dev/null +++ b/java/res/values/additional-proximitychars.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* +** +** Copyright 2012, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ +--> +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string-array name="additional_proximitychars"> + </string-array> +</resources> diff --git a/java/src/com/android/inputmethod/keyboard/Key.java b/java/src/com/android/inputmethod/keyboard/Key.java index 5e58821d0..8f2efab29 100644 --- a/java/src/com/android/inputmethod/keyboard/Key.java +++ b/java/src/com/android/inputmethod/keyboard/Key.java @@ -28,8 +28,9 @@ import android.util.Xml; import com.android.inputmethod.keyboard.internal.KeyStyles; import com.android.inputmethod.keyboard.internal.KeyStyles.KeyStyle; import com.android.inputmethod.keyboard.internal.KeyboardIconsSet; -import com.android.inputmethod.keyboard.internal.MoreKeySpecParser; +import com.android.inputmethod.keyboard.internal.KeySpecParser; import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.Utils; import com.android.inputmethod.latin.XmlParseUtils; import org.xmlpull.v1.XmlPullParser; @@ -128,7 +129,7 @@ public class Key { private boolean mEnabled = true; private static Drawable getIcon(Keyboard.Params params, String moreKeySpec) { - final int iconAttrId = MoreKeySpecParser.getIconAttrId(moreKeySpec); + final int iconAttrId = KeySpecParser.getIconAttrId(moreKeySpec); if (iconAttrId == KeyboardIconsSet.ICON_UNDEFINED) { return null; } else { @@ -141,9 +142,9 @@ public class Key { */ public Key(Resources res, Keyboard.Params params, String moreKeySpec, int x, int y, int width, int height) { - this(params, MoreKeySpecParser.getLabel(moreKeySpec), null, getIcon(params, moreKeySpec), - MoreKeySpecParser.getCode(res, moreKeySpec), - MoreKeySpecParser.getOutputText(moreKeySpec), + this(params, KeySpecParser.getLabel(moreKeySpec), null, getIcon(params, moreKeySpec), + KeySpecParser.getCode(res, moreKeySpec), + KeySpecParser.getOutputText(moreKeySpec), x, y, width, height); } @@ -245,7 +246,7 @@ public class Key { int actionFlags = style.getFlag(keyAttr, R.styleable.Keyboard_Key_keyActionFlags, 0); final String[] additionalMoreKeys = style.getStringArray( keyAttr, R.styleable.Keyboard_Key_additionalMoreKeys); - final String[] moreKeys = MoreKeySpecParser.insertAddtionalMoreKeys(style.getStringArray( + final String[] moreKeys = KeySpecParser.insertAddtionalMoreKeys(style.getStringArray( keyAttr, R.styleable.Keyboard_Key_moreKeys), additionalMoreKeys); if (moreKeys != null) { actionFlags |= ACTION_FLAGS_ENABLE_LONG_PRESS; @@ -270,7 +271,7 @@ public class Key { // Choose the first letter of the label as primary code if not specified. if (code == Keyboard.CODE_UNSPECIFIED && TextUtils.isEmpty(outputText) && !TextUtils.isEmpty(mLabel)) { - if (mLabel.codePointCount(0, mLabel.length()) == 1) { + if (Utils.codePointCount(mLabel) == 1) { // Use the first letter of the hint label if shiftedLetterActivated flag is // specified. if (hasShiftedLetterHint() && isShiftedLetterActivated() @@ -308,7 +309,7 @@ public class Key { if (!Keyboard.isLetterCode(code) || preserveCase) return code; final String text = new String(new int[] { code } , 0, 1); final String casedText = adjustCaseOfStringForKeyboardId(text, preserveCase, id); - return casedText.codePointCount(0, casedText.length()) == 1 + return Utils.codePointCount(casedText) == 1 ? casedText.codePointAt(0) : Keyboard.CODE_UNSPECIFIED; } @@ -380,7 +381,7 @@ public class Key { @Override public String toString() { String top = Keyboard.printableCode(mCode); - if (mLabel != null && mLabel.codePointCount(0, mLabel.length()) != 1) { + if (Utils.codePointCount(mLabel) != 1) { top += "/\"" + mLabel + '"'; } return String.format("%s %d,%d", top, mX, mY); diff --git a/java/src/com/android/inputmethod/keyboard/KeyDetector.java b/java/src/com/android/inputmethod/keyboard/KeyDetector.java index 0d271625b..bff491ffd 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyDetector.java +++ b/java/src/com/android/inputmethod/keyboard/KeyDetector.java @@ -19,12 +19,14 @@ package com.android.inputmethod.keyboard; import android.util.Log; import java.util.Arrays; +import java.util.List; public class KeyDetector { private static final String TAG = KeyDetector.class.getSimpleName(); private static final boolean DEBUG = false; public static final int NOT_A_CODE = -1; + private static final int ADDITIONAL_PROXIMITY_CHAR_DELIMITER_CODE = 2; private final int mKeyHysteresisDistanceSquared; @@ -154,8 +156,9 @@ public class KeyDetector { return distances.length; } - private void getNearbyKeyCodes(final int[] allCodes) { + private void getNearbyKeyCodes(final int primaryCode, final int[] allCodes) { final Key[] neighborKeys = mNeighborKeys; + final int maxCodesSize = allCodes.length; // allCodes[0] should always have the key code even if it is a non-letter key. if (neighborKeys[0] == null) { @@ -164,7 +167,7 @@ public class KeyDetector { } int numCodes = 0; - for (int j = 0; j < neighborKeys.length && numCodes < allCodes.length; j++) { + for (int j = 0; j < neighborKeys.length && numCodes < maxCodesSize; j++) { final Key key = neighborKeys[j]; if (key == null) break; @@ -174,6 +177,38 @@ public class KeyDetector { continue; allCodes[numCodes++] = code; } + if (maxCodesSize <= numCodes) { + return; + } + if (primaryCode != NOT_A_CODE) { + final List<Integer> additionalChars = + mKeyboard.getAdditionalProximityChars().get(primaryCode); + if (additionalChars == null || additionalChars.size() == 0) { + return; + } + int currentCodesSize = numCodes; + allCodes[numCodes++] = ADDITIONAL_PROXIMITY_CHAR_DELIMITER_CODE; + if (maxCodesSize <= numCodes) { + return; + } + // TODO: This is O(N^2). Assuming additionalChars.size() is up to 4 or 5. + for (int i = 0; i < additionalChars.size(); ++i) { + final int additionalChar = additionalChars.get(i); + boolean contains = false; + for (int j = 0; j < currentCodesSize; ++j) { + if (additionalChar == allCodes[j]) { + contains = true; + break; + } + } + if (!contains) { + allCodes[numCodes++] = additionalChar; + if (maxCodesSize <= numCodes) { + return; + } + } + } + } } /** @@ -205,7 +240,7 @@ public class KeyDetector { } if (allCodes != null && allCodes.length > 0) { - getNearbyKeyCodes(allCodes); + getNearbyKeyCodes(primaryKey != null ? primaryKey.mCode : NOT_A_CODE, allCodes); if (DEBUG) { Log.d(TAG, "x=" + x + " y=" + y + " primary=" + printableCode(primaryKey) diff --git a/java/src/com/android/inputmethod/keyboard/Keyboard.java b/java/src/com/android/inputmethod/keyboard/Keyboard.java index 6653dec4b..10e0a5b01 100644 --- a/java/src/com/android/inputmethod/keyboard/Keyboard.java +++ b/java/src/com/android/inputmethod/keyboard/Keyboard.java @@ -20,6 +20,7 @@ import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; +import android.text.TextUtils; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.Log; @@ -38,10 +39,12 @@ import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; @@ -130,6 +133,8 @@ public class Keyboard { private final ProximityInfo mProximityInfo; + public final Map<Integer, List<Integer>> mAdditionalProximityChars; + public Keyboard(Params params) { mId = params.mId; mThemeId = params.mThemeId; @@ -146,10 +151,12 @@ public class Keyboard { mKeys = Collections.unmodifiableSet(params.mKeys); mShiftKeys = Collections.unmodifiableSet(params.mShiftKeys); mIconsSet = params.mIconsSet; + mAdditionalProximityChars = params.mAdditionalProximityChars; mProximityInfo = new ProximityInfo( params.GRID_WIDTH, params.GRID_HEIGHT, mOccupiedWidth, mOccupiedHeight, - mMostCommonKeyWidth, mMostCommonKeyHeight, mKeys, params.mTouchPositionCorrection); + mMostCommonKeyWidth, mMostCommonKeyHeight, mKeys, params.mTouchPositionCorrection, + params.mAdditionalProximityChars); } public ProximityInfo getProximityInfo() { @@ -227,6 +234,9 @@ public class Keyboard { public final Set<Key> mKeys = new HashSet<Key>(); public final Set<Key> mShiftKeys = new HashSet<Key>(); public final KeyboardIconsSet mIconsSet = new KeyboardIconsSet(); + // TODO: Should be in Key instead of Keyboard.Params? + public final Map<Integer, List<Integer>> mAdditionalProximityChars = + new HashMap<Integer, List<Integer>>(); public KeyboardSet.KeysCache mKeysCache; @@ -358,6 +368,10 @@ public class Keyboard { return mProximityInfo.getNearestKeys(adjustedX, adjustedY); } + public Map<Integer, List<Integer>> getAdditionalProximityChars() { + return mAdditionalProximityChars; + } + public static String printableCode(int code) { switch (code) { case CODE_SHIFT: return "shift"; @@ -614,6 +628,7 @@ public class Keyboard { mParams = params; setTouchPositionCorrectionData(context, params); + setAdditionalProximityChars(context, params); params.GRID_WIDTH = res.getInteger(R.integer.config_keyboard_grid_width); params.GRID_HEIGHT = res.getInteger(R.integer.config_keyboard_grid_height); @@ -636,6 +651,25 @@ public class Keyboard { params.mTouchPositionCorrection.load(data); } + private static void setAdditionalProximityChars(Context context, Params params) { + final String[] additionalChars = + context.getResources().getStringArray(R.array.additional_proximitychars); + int currentPrimaryIndex = 0; + for (int i = 0; i < additionalChars.length; ++i) { + final String additionalChar = additionalChars[i]; + if (TextUtils.isEmpty(additionalChar)) { + currentPrimaryIndex = 0; + } else if (currentPrimaryIndex == 0) { + currentPrimaryIndex = additionalChar.charAt(0); + params.mAdditionalProximityChars.put( + currentPrimaryIndex, new ArrayList<Integer>()); + } else if (currentPrimaryIndex != 0) { + final int c = additionalChar.charAt(0); + params.mAdditionalProximityChars.get(currentPrimaryIndex).add(c); + } + } + } + public void setAutoGenerate(KeyboardSet.KeysCache keysCache) { mParams.mKeysCache = keysCache; } diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardView.java b/java/src/com/android/inputmethod/keyboard/KeyboardView.java index 2cbd132ca..c6fb75489 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyboardView.java +++ b/java/src/com/android/inputmethod/keyboard/KeyboardView.java @@ -41,6 +41,7 @@ import com.android.inputmethod.compat.FrameLayoutCompatUtils; import com.android.inputmethod.latin.LatinImeLogger; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.StaticInnerHandlerWrapper; +import com.android.inputmethod.latin.Utils; import java.util.HashMap; @@ -851,7 +852,7 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { if (key.mLabel != null) { // TODO Should take care of temporaryShiftLabel here. previewText.setCompoundDrawables(null, null, null, null); - if (key.mLabel.length() > 1) { + if (Utils.codePointCount(key.mLabel) > 1) { previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, params.mKeyLetterSize); previewText.setTypeface(Typeface.DEFAULT_BOLD); } else { diff --git a/java/src/com/android/inputmethod/keyboard/MiniKeyboard.java b/java/src/com/android/inputmethod/keyboard/MiniKeyboard.java index 433bd0d75..4648da1c1 100644 --- a/java/src/com/android/inputmethod/keyboard/MiniKeyboard.java +++ b/java/src/com/android/inputmethod/keyboard/MiniKeyboard.java @@ -18,7 +18,7 @@ package com.android.inputmethod.keyboard; import android.graphics.Paint; -import com.android.inputmethod.keyboard.internal.MoreKeySpecParser; +import com.android.inputmethod.keyboard.internal.KeySpecParser; import com.android.inputmethod.latin.R; public class MiniKeyboard extends Keyboard { @@ -235,7 +235,7 @@ public class MiniKeyboard extends Keyboard { Paint paint = null; int maxWidth = minKeyWidth; for (String moreKeySpec : moreKeys) { - final String label = MoreKeySpecParser.getLabel(moreKeySpec); + final String label = KeySpecParser.getLabel(moreKeySpec); // If the label is single letter, minKeyWidth is enough to hold the label. if (label != null && label.length() > 1) { if (paint == null) { diff --git a/java/src/com/android/inputmethod/keyboard/ProximityInfo.java b/java/src/com/android/inputmethod/keyboard/ProximityInfo.java index c1dae0601..2d1a0083d 100644 --- a/java/src/com/android/inputmethod/keyboard/ProximityInfo.java +++ b/java/src/com/android/inputmethod/keyboard/ProximityInfo.java @@ -24,6 +24,9 @@ import com.android.inputmethod.latin.spellcheck.SpellCheckerProximityInfo; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.Set; public class ProximityInfo { @@ -44,7 +47,8 @@ public class ProximityInfo { private final Key[][] mGridNeighbors; ProximityInfo(int gridWidth, int gridHeight, int minWidth, int height, int keyWidth, - int keyHeight, Set<Key> keys, TouchPositionCorrection touchPositionCorrection) { + int keyHeight, Set<Key> keys, TouchPositionCorrection touchPositionCorrection, + Map<Integer, List<Integer>> additionalProximityChars) { mGridWidth = gridWidth; mGridHeight = gridHeight; mGridSize = mGridWidth * mGridHeight; @@ -58,20 +62,20 @@ public class ProximityInfo { // No proximity required. Keyboard might be mini keyboard. return; } - computeNearestNeighbors(keyWidth, keys, touchPositionCorrection); + computeNearestNeighbors(keyWidth, keys, touchPositionCorrection, additionalProximityChars); } public static ProximityInfo createDummyProximityInfo() { - return new ProximityInfo(1, 1, 1, 1, 1, 1, Collections.<Key>emptySet(), null); + return new ProximityInfo(1, 1, 1, 1, 1, 1, Collections.<Key> emptySet(), + null, Collections.<Integer, List<Integer>> emptyMap()); } public static ProximityInfo createSpellCheckerProximityInfo(final int[] proximity) { final ProximityInfo spellCheckerProximityInfo = createDummyProximityInfo(); spellCheckerProximityInfo.mNativeProximityInfo = spellCheckerProximityInfo.setProximityInfoNative( - SpellCheckerProximityInfo.ROW_SIZE, - 480, 300, 11, 3, proximity, - 0, null, null, null, null, null, null, null, null); + SpellCheckerProximityInfo.ROW_SIZE, 480, 300, 11, 3, proximity, 0, + null, null, null, null, null, null, null, null); return spellCheckerProximityInfo; } @@ -79,11 +83,13 @@ public class ProximityInfo { static { Utils.loadNativeLibrary(); } + private native long setProximityInfoNative(int maxProximityCharsSize, int displayWidth, int displayHeight, int gridWidth, int gridHeight, int[] proximityCharsArray, int keyCount, int[] keyXCoordinates, int[] keyYCoordinates, int[] keyWidths, int[] keyHeights, int[] keyCharCodes, float[] sweetSpotCenterX, float[] sweetSpotCenterY, float[] sweetSpotRadii); + private native void releaseProximityInfoNative(long nativeProximityInfo); private final void setProximityInfo(Key[][] gridNeighborKeys, int keyboardWidth, @@ -138,7 +144,7 @@ public class ProximityInfo { final float radius = touchPositionCorrection.mRadii[row]; sweetSpotCenterXs[i] = hitBoxCenterX + x * hitBoxWidth; sweetSpotCenterYs[i] = hitBoxCenterY + y * hitBoxHeight; - sweetSpotRadii[i] = radius * (float)Math.sqrt( + sweetSpotRadii[i] = radius * (float) Math.sqrt( hitBoxWidth * hitBoxWidth + hitBoxHeight * hitBoxHeight); } } @@ -168,7 +174,12 @@ public class ProximityInfo { } private void computeNearestNeighbors(int defaultWidth, Set<Key> keys, - TouchPositionCorrection touchPositionCorrection) { + TouchPositionCorrection touchPositionCorrection, + Map<Integer, List<Integer>> additionalProximityChars) { + final Map<Integer, Key> keyCodeMap = new HashMap<Integer, Key>(); + for (final Key key : keys) { + keyCodeMap.put(key.mCode, key); + } final int thresholdBase = (int) (defaultWidth * SEARCH_DISTANCE); final int threshold = thresholdBase * thresholdBase; // Round-up so we don't have any pixels outside the grid @@ -186,6 +197,27 @@ public class ProximityInfo { neighborKeys[count++] = key; } } + int currentCodesSize = count; + for (int i = 0; i < currentCodesSize; ++i) { + final int c = neighborKeys[i].mCode; + final List<Integer> additionalChars = additionalProximityChars.get(c); + if (additionalChars == null || additionalChars.size() == 0) { + continue; + } + for (int j = 0; j < additionalChars.size(); ++j) { + final int additionalChar = additionalChars.get(j); + boolean contains = false; + for (int k = 0; k < count; ++k) { + if(additionalChar == neighborKeys[k].mCode) { + contains = true; + break; + } + } + if (!contains) { + neighborKeys[count++] = keyCodeMap.get(additionalChar); + } + } + } mGridNeighbors[(y / mCellHeight) * mGridWidth + (x / mCellWidth)] = Arrays.copyOfRange(neighborKeys, 0, count); } @@ -199,7 +231,7 @@ public class ProximityInfo { return EMPTY_KEY_ARRAY; } if (x >= 0 && x < mKeyboardMinWidth && y >= 0 && y < mKeyboardHeight) { - int index = (y / mCellHeight) * mGridWidth + (x / mCellWidth); + int index = (y / mCellHeight) * mGridWidth + (x / mCellWidth); if (index < mGridSize) { return mGridNeighbors[index]; } diff --git a/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpecParser.java b/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java index abebfec01..ba12676ad 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpecParser.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java @@ -30,23 +30,31 @@ import java.util.Arrays; /** * String parser of moreKeys attribute of Key. * The string is comma separated texts each of which represents one "more key". + * - String resource can be embedded into specification @string/name. This is done before parsing + * comma. * Each "more key" specification is one of the following: * - A single letter (Letter) * - Label optionally followed by keyOutputText or code (keyLabel|keyOutputText). * - Icon followed by keyOutputText or code (@icon/icon_name|@integer/key_code) - * Special character, comma ',' backslash '\', and bar '|' can be escaped by '\' - * character. + * Special character, comma ',' backslash '\', and bar '|' can be escaped by '\' character. * Note that the character '@' and '\' are also parsed by XML parser and CSV parser as well. - * See {@link KeyboardIconsSet} about icon_number. + * See {@link KeyboardIconsSet} about icon_name. */ -public class MoreKeySpecParser { +public class KeySpecParser { private static final boolean DEBUG = LatinImeLogger.sDBG; + + // Constants for parsing. + private static int COMMA = ','; + private static final char ESCAPE_CHAR = '\\'; + private static final char PREFIX_AT = '@'; + private static final char SUFFIX_SLASH = '/'; + private static final String PREFIX_STRING = PREFIX_AT + "string"; private static final char LABEL_END = '|'; - private static final String PREFIX_ICON = Utils.PREFIX_AT + "icon" + Utils.SUFFIX_SLASH; - private static final String PREFIX_CODE = Utils.PREFIX_AT + "integer" + Utils.SUFFIX_SLASH; + private static final String PREFIX_ICON = PREFIX_AT + "icon" + SUFFIX_SLASH; + private static final String PREFIX_CODE = PREFIX_AT + "integer" + SUFFIX_SLASH; private static final String ADDITIONAL_MORE_KEY_MARKER = "%"; - private MoreKeySpecParser() { + private KeySpecParser() { // Intentional empty constructor for utility class. } @@ -56,7 +64,7 @@ public class MoreKeySpecParser { if (end > 0) { return true; } - throw new MoreKeySpecParserError("outputText or code not specified: " + moreKeySpec); + throw new KeySpecParserError("outputText or code not specified: " + moreKeySpec); } return false; } @@ -71,15 +79,17 @@ public class MoreKeySpecParser { } private static String parseEscape(String text) { - if (text.indexOf(Utils.ESCAPE_CHAR) < 0) { + if (text.indexOf(ESCAPE_CHAR) < 0) { return text; } final int length = text.length(); final StringBuilder sb = new StringBuilder(); for (int pos = 0; pos < length; pos++) { final char c = text.charAt(pos); - if (c == Utils.ESCAPE_CHAR && pos + 1 < length) { - sb.append(text.charAt(++pos)); + if (c == ESCAPE_CHAR && pos + 1 < length) { + // Skip escape char + pos++; + sb.append(text.charAt(pos)); } else { sb.append(c); } @@ -88,17 +98,18 @@ public class MoreKeySpecParser { } private static int indexOfLabelEnd(String moreKeySpec, int start) { - if (moreKeySpec.indexOf(Utils.ESCAPE_CHAR, start) < 0) { + if (moreKeySpec.indexOf(ESCAPE_CHAR, start) < 0) { final int end = moreKeySpec.indexOf(LABEL_END, start); if (end == 0) { - throw new MoreKeySpecParserError(LABEL_END + " at " + start + ": " + moreKeySpec); + throw new KeySpecParserError(LABEL_END + " at " + start + ": " + moreKeySpec); } return end; } final int length = moreKeySpec.length(); for (int pos = start; pos < length; pos++) { final char c = moreKeySpec.charAt(pos); - if (c == Utils.ESCAPE_CHAR && pos + 1 < length) { + if (c == ESCAPE_CHAR && pos + 1 < length) { + // Skip escape char pos++; } else if (c == LABEL_END) { return pos; @@ -115,7 +126,7 @@ public class MoreKeySpecParser { final String label = (end > 0) ? parseEscape(moreKeySpec.substring(0, end)) : parseEscape(moreKeySpec); if (TextUtils.isEmpty(label)) { - throw new MoreKeySpecParserError("Empty label: " + moreKeySpec); + throw new KeySpecParserError("Empty label: " + moreKeySpec); } return label; } @@ -127,7 +138,7 @@ public class MoreKeySpecParser { final int end = indexOfLabelEnd(moreKeySpec, 0); if (end > 0) { if (indexOfLabelEnd(moreKeySpec, end + 1) >= 0) { - throw new MoreKeySpecParserError("Multiple " + LABEL_END + ": " + throw new KeySpecParserError("Multiple " + LABEL_END + ": " + moreKeySpec); } final String outputText = parseEscape( @@ -135,23 +146,23 @@ public class MoreKeySpecParser { if (!TextUtils.isEmpty(outputText)) { return outputText; } - throw new MoreKeySpecParserError("Empty outputText: " + moreKeySpec); + throw new KeySpecParserError("Empty outputText: " + moreKeySpec); } final String label = getLabel(moreKeySpec); if (label == null) { - throw new MoreKeySpecParserError("Empty label: " + moreKeySpec); + throw new KeySpecParserError("Empty label: " + moreKeySpec); } // Code is automatically generated for one letter label. See {@link getCode()}. - return (label.length() == 1) ? null : label; + return (Utils.codePointCount(label) == 1) ? null : label; } public static int getCode(Resources res, String moreKeySpec) { if (hasCode(moreKeySpec)) { final int end = indexOfLabelEnd(moreKeySpec, 0); if (indexOfLabelEnd(moreKeySpec, end + 1) >= 0) { - throw new MoreKeySpecParserError("Multiple " + LABEL_END + ": " + moreKeySpec); + throw new KeySpecParserError("Multiple " + LABEL_END + ": " + moreKeySpec); } - final int resId = Utils.getResourceId(res, + final int resId = getResourceId(res, moreKeySpec.substring(end + /* LABEL_END */1 + /* PREFIX_AT */1), R.string.english_ime_name); final int code = res.getInteger(resId); @@ -162,8 +173,8 @@ public class MoreKeySpecParser { } final String label = getLabel(moreKeySpec); // Code is automatically generated for one letter label. - if (label != null && label.length() == 1) { - return label.charAt(0); + if (Utils.codePointCount(label) == 1) { + return label.codePointAt(0); } return Keyboard.CODE_OUTPUT_TEXT; } @@ -248,9 +259,128 @@ public class MoreKeySpecParser { } @SuppressWarnings("serial") - public static class MoreKeySpecParserError extends RuntimeException { - public MoreKeySpecParserError(String message) { + public static class KeySpecParserError extends RuntimeException { + public KeySpecParserError(String message) { super(message); } } + + private static int getResourceId(Resources res, String name, int packageNameResId) { + String packageName = res.getResourcePackageName(packageNameResId); + int resId = res.getIdentifier(name, null, packageName); + if (resId == 0) { + throw new RuntimeException("Unknown resource: " + name); + } + return resId; + } + + private static String resolveStringResource(String text, Resources res, int packageNameResId) { + final int size = text.length(); + if (size < PREFIX_STRING.length()) { + return text; + } + + StringBuilder sb = null; + for (int pos = 0; pos < size; pos++) { + final char c = text.charAt(pos); + if (c == PREFIX_AT && text.startsWith(PREFIX_STRING, pos)) { + if (sb == null) { + sb = new StringBuilder(text.substring(0, pos)); + } + final int end = searchResourceNameEnd(text, pos + PREFIX_STRING.length()); + final String resName = text.substring(pos + 1, end); + final int resId = getResourceId(res, resName, packageNameResId); + sb.append(res.getString(resId)); + pos = end - 1; + } else if (c == ESCAPE_CHAR) { + pos++; + if (sb != null) { + sb.append(c); + if (pos < size) { + sb.append(text.charAt(pos)); + } + } + } else if (sb != null) { + sb.append(c); + } + } + return (sb == null) ? text : sb.toString(); + } + + private static int searchResourceNameEnd(String text, int start) { + final int size = text.length(); + if (start >= size || text.charAt(start) != SUFFIX_SLASH) { + throw new RuntimeException("Resource name not specified"); + } + for (int pos = start + 1; pos < size; pos++) { + final char c = text.charAt(pos); + // String resource name should be consisted of [a-z_0-9]. + if ((c >= 'a' && c <= 'z') || c == '_' || (c >= '0' && c <= '9')) { + continue; + } + return pos; + } + return size; + } + + public static String[] parseCsvString(String rawText, Resources res, int packageNameResId) { + final String text = resolveStringResource(rawText, res, packageNameResId); + final int size = text.length(); + if (size == 0) { + return null; + } + if (Utils.codePointCount(text) == 1) { + return new String[] { text }; + } + + final StringBuilder sb = new StringBuilder(); + ArrayList<String> list = null; + int start = 0; + for (int pos = 0; pos < size; pos++) { + final char c = text.charAt(pos); + if (c == COMMA) { + if (list == null) { + list = new ArrayList<String>(); + } + if (sb.length() == 0) { + list.add(text.substring(start, pos)); + } else { + list.add(sb.toString()); + sb.setLength(0); + } + // Skip comma + start = pos + 1; + continue; + } + // TODO: Only parse escaped comma. Other escaped character should be passed through + // with escaped character prefixed. + // Skip escaped sequence. + if (c == ESCAPE_CHAR) { + if (start == pos) { + // Skip escaping comma at the beginning of the text. + start++; + pos++; + } else { + if (start < pos && sb.length() == 0) { + sb.append(text.substring(start, pos)); + } + // Skip comma + pos++; + if (pos < size) { + sb.append(text.charAt(pos)); + } + } + } else if (sb.length() > 0) { + sb.append(c); + } + } + if (list == null) { + return new String[] { + sb.length() > 0 ? sb.toString() : text.substring(start) + }; + } else { + list.add(sb.length() > 0 ? sb.toString() : text.substring(start)); + return list.toArray(new String[list.size()]); + } + } } diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyStyles.java b/java/src/com/android/inputmethod/keyboard/internal/KeyStyles.java index 25a2c23c4..6ec56ca9f 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyStyles.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyStyles.java @@ -21,7 +21,6 @@ import android.util.Log; import com.android.inputmethod.keyboard.Keyboard; import com.android.inputmethod.latin.R; -import com.android.inputmethod.latin.Utils; import com.android.inputmethod.latin.XmlParseUtils; import org.xmlpull.v1.XmlPullParser; @@ -72,7 +71,7 @@ public class KeyStyles { protected static String[] parseStringArray(TypedArray a, int index) { if (!a.hasValue(index)) return null; - return Utils.parseCsvString( + return KeySpecParser.parseCsvString( a.getString(index), a.getResources(), R.string.english_ime_name); } } diff --git a/java/src/com/android/inputmethod/latin/SettingsValues.java b/java/src/com/android/inputmethod/latin/SettingsValues.java index 5f9cb8df6..8e2f605c4 100644 --- a/java/src/com/android/inputmethod/latin/SettingsValues.java +++ b/java/src/com/android/inputmethod/latin/SettingsValues.java @@ -25,7 +25,7 @@ import android.view.inputmethod.EditorInfo; import com.android.inputmethod.compat.InputTypeCompatUtils; import com.android.inputmethod.compat.VibratorCompatWrapper; -import com.android.inputmethod.keyboard.internal.MoreKeySpecParser; +import com.android.inputmethod.keyboard.internal.KeySpecParser; import java.util.Arrays; import java.util.Locale; @@ -100,7 +100,7 @@ public class SettingsValues { } } } - final String[] suggestPuncsSpec = Utils.parseCsvString( + final String[] suggestPuncsSpec = KeySpecParser.parseCsvString( res.getString(R.string.suggested_punctuations), res, R.string.english_ime_name); mSuggestPuncs = createSuggestPuncs(suggestPuncsSpec); mSuggestPuncList = createSuggestPuncList(suggestPuncsSpec); @@ -158,7 +158,7 @@ public class SettingsValues { final StringBuilder sb = new StringBuilder(); if (puncs != null) { for (final String puncSpec : puncs) { - sb.append(MoreKeySpecParser.getLabel(puncSpec)); + sb.append(KeySpecParser.getLabel(puncSpec)); } } return sb.toString(); @@ -168,7 +168,7 @@ public class SettingsValues { final SuggestedWords.Builder builder = new SuggestedWords.Builder(); if (puncs != null) { for (final String puncSpec : puncs) { - builder.addWord(MoreKeySpecParser.getLabel(puncSpec)); + builder.addWord(KeySpecParser.getLabel(puncSpec)); } } return builder.setIsPunctuationSuggestions().build(); @@ -178,11 +178,11 @@ public class SettingsValues { final SuggestedWords.Builder builder = new SuggestedWords.Builder(); if (puncs != null) { for (final String puncSpec : puncs) { - final String outputText = MoreKeySpecParser.getOutputText(puncSpec); + final String outputText = KeySpecParser.getOutputText(puncSpec); if (outputText != null) { builder.addWord(outputText); } else { - builder.addWord(MoreKeySpecParser.getLabel(puncSpec)); + builder.addWord(KeySpecParser.getLabel(puncSpec)); } } } diff --git a/java/src/com/android/inputmethod/latin/Utils.java b/java/src/com/android/inputmethod/latin/Utils.java index d1b808fc8..3975dddeb 100644 --- a/java/src/com/android/inputmethod/latin/Utils.java +++ b/java/src/com/android/inputmethod/latin/Utils.java @@ -62,12 +62,6 @@ public class Utils { private static boolean DBG = LatinImeLogger.sDBG; private static boolean DBG_EDIT_DISTANCE = false; - // Constants for resource name parsing. - public static final char ESCAPE_CHAR = '\\'; - public static final char PREFIX_AT = '@'; - public static final char SUFFIX_SLASH = '/'; - private static final String PREFIX_STRING = PREFIX_AT + "string"; - private Utils() { // Intentional empty constructor for utility class. } @@ -800,116 +794,8 @@ public class Utils { } } - public static int getResourceId(Resources res, String name, int packageNameResId) { - String packageName = res.getResourcePackageName(packageNameResId); - int resId = res.getIdentifier(name, null, packageName); - if (resId == 0) { - throw new RuntimeException("Unknown resource: " + name); - } - return resId; - } - - public static String resolveStringResource(String text, Resources res, int packageNameResId) { - final int size = text.length(); - if (size < PREFIX_STRING.length()) { - return text; - } - - StringBuilder sb = null; - for (int pos = 0; pos < size; pos++) { - final char c = text.charAt(pos); - if (c == PREFIX_AT && text.startsWith(PREFIX_STRING, pos)) { - if (sb == null) { - sb = new StringBuilder(text.substring(0, pos)); - } - final int end = Utils.searchResourceNameEnd(text, pos + PREFIX_STRING.length()); - final String resName = text.substring(pos + 1, end); - final int resId = getResourceId(res, resName, packageNameResId); - sb.append(res.getString(resId)); - pos = end - 1; - } else if (c == ESCAPE_CHAR) { - pos++; - if (sb != null) { - sb.append(c); - if (pos < size) { - sb.append(text.charAt(pos)); - } - } - } else if (sb != null) { - sb.append(c); - } - } - return (sb == null) ? text : sb.toString(); - } - - private static int searchResourceNameEnd(String text, int start) { - final int size = text.length(); - if (start >= size || text.charAt(start) != SUFFIX_SLASH) { - throw new RuntimeException("Resource name not specified"); - } - for (int pos = start + 1; pos < size; pos++) { - final char c = text.charAt(pos); - // String resource name should be consisted of [a-z_0-9]. - if ((c >= 'a' && c <= 'z') || c == '_' || (c >= '0' && c <= '9')) { - continue; - } - return pos; - } - return size; - } - - public static String[] parseCsvString(String rawText, Resources res, int packageNameResId) { - final String text = resolveStringResource(rawText, res, packageNameResId); - final int size = text.length(); - if (size == 0) { - return null; - } - if (size == 1) { - return new String[] { text }; - } - - final StringBuilder sb = new StringBuilder(); - ArrayList<String> list = null; - int start = 0; - for (int pos = 0; pos < size; pos++) { - final char c = text.charAt(pos); - if (c == ',') { - if (list == null) { - list = new ArrayList<String>(); - } - if (sb.length() == 0) { - list.add(text.substring(start, pos)); - } else { - list.add(sb.toString()); - sb.setLength(0); - } - start = pos + 1; - continue; - } else if (c == ESCAPE_CHAR) { - if (start == pos) { - // Skip escape character at the beginning of the value. - start++; - pos++; - } else { - if (start < pos && sb.length() == 0) { - sb.append(text.subSequence(start, pos)); - } - pos++; - if (pos < size) { - sb.append(text.charAt(pos)); - } - } - } else if (sb.length() > 0) { - sb.append(c); - } - } - if (list == null) { - return new String[] { - sb.length() > 0 ? sb.toString() : text.substring(start) - }; - } else { - list.add(sb.length() > 0 ? sb.toString() : text.substring(start)); - return list.toArray(new String[list.size()]); - } + public static int codePointCount(String text) { + if (TextUtils.isEmpty(text)) return 0; + return text.codePointCount(0, text.length()); } } diff --git a/java/src/com/android/inputmethod/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java index 230c2916b..bd244b913 100644 --- a/java/src/com/android/inputmethod/latin/WordComposer.java +++ b/java/src/com/android/inputmethod/latin/WordComposer.java @@ -16,8 +16,6 @@ package com.android.inputmethod.latin; -import android.text.TextUtils; - import com.android.inputmethod.keyboard.Key; import com.android.inputmethod.keyboard.KeyDetector; import com.android.inputmethod.keyboard.Keyboard; @@ -33,7 +31,7 @@ public class WordComposer { public static final int NOT_A_CODE = KeyDetector.NOT_A_CODE; public static final int NOT_A_COORDINATE = -1; - final int N = BinaryDictionary.MAX_WORD_LENGTH; + final static int N = BinaryDictionary.MAX_WORD_LENGTH; private ArrayList<int[]> mCodes; private int[] mXCoordinates; diff --git a/native/src/correction.cpp b/native/src/correction.cpp index 7323747d7..dafc0fd7f 100644 --- a/native/src/correction.cpp +++ b/native/src/correction.cpp @@ -383,7 +383,10 @@ Correction::CorrectionType Correction::processCharAndCalcState( incrementInputIndex(); } else { --mTransposedCount; - if (DEBUG_CORRECTION) { + if (DEBUG_CORRECTION + && (INPUTLENGTH_FOR_DEBUG <= 0 || INPUTLENGTH_FOR_DEBUG == mInputLength) + && (MIN_OUTPUT_INDEX_FOR_DEBUG <= 0 + || MIN_OUTPUT_INDEX_FOR_DEBUG < mOutputIndex)) { DUMP_WORD(mWord, mOutputIndex); AKLOGI("UNRELATED(0): %d, %d, %d, %d, %c", mProximityCount, mSkippedCount, mTransposedCount, mExcessiveCount, c); @@ -404,13 +407,17 @@ Correction::CorrectionType Correction::processCharAndCalcState( : mProximityInfo->getMatchedProximityId( mInputIndex, c, checkProximityChars, &proximityIndex); - if (ProximityInfo::UNRELATED_CHAR == matchedProximityCharId) { + if (ProximityInfo::UNRELATED_CHAR == matchedProximityCharId + || ProximityInfo::ADDITIONAL_PROXIMITY_CHAR == matchedProximityCharId) { if (canTryCorrection && mOutputIndex > 0 && mCorrectionStates[mOutputIndex].mProximityMatching && mCorrectionStates[mOutputIndex].mExceeding && isEquivalentChar(mProximityInfo->getMatchedProximityId( mInputIndex, mWord[mOutputIndex - 1], false))) { - if (DEBUG_CORRECTION) { + if (DEBUG_CORRECTION + && (INPUTLENGTH_FOR_DEBUG <= 0 || INPUTLENGTH_FOR_DEBUG == mInputLength) + && (MIN_OUTPUT_INDEX_FOR_DEBUG <= 0 + || MIN_OUTPUT_INDEX_FOR_DEBUG < mOutputIndex)) { AKLOGI("CONVERSION p->e %c", mWord[mOutputIndex - 1]); } // Conversion p->e @@ -429,7 +436,8 @@ Correction::CorrectionType Correction::processCharAndCalcState( } } - if (ProximityInfo::UNRELATED_CHAR == matchedProximityCharId) { + if (ProximityInfo::UNRELATED_CHAR == matchedProximityCharId + || ProximityInfo::ADDITIONAL_PROXIMITY_CHAR == matchedProximityCharId) { // TODO: Optimize // As the current char turned out to be an unrelated char, // we will try other correction-types. Please note that mCorrectionStates[mOutputIndex] @@ -481,12 +489,47 @@ Correction::CorrectionType Correction::processCharAndCalcState( ++mExcessiveCount; incrementInputIndex(); } + if (DEBUG_CORRECTION + && (INPUTLENGTH_FOR_DEBUG <= 0 || INPUTLENGTH_FOR_DEBUG == mInputLength) + && (MIN_OUTPUT_INDEX_FOR_DEBUG <= 0 + || MIN_OUTPUT_INDEX_FOR_DEBUG < mOutputIndex)) { + DUMP_WORD(mWord, mOutputIndex); + if (mTransposing) { + AKLOGI("TRANSPOSE: %d, %d, %d, %d, %c", mProximityCount, mSkippedCount, + mTransposedCount, mExcessiveCount, c); + } else { + AKLOGI("EXCEED: %d, %d, %d, %d, %c", mProximityCount, mSkippedCount, + mTransposedCount, mExcessiveCount, c); + } + } } else if (mSkipping) { // 3. Skip correction ++mSkippedCount; + if (DEBUG_CORRECTION + && (INPUTLENGTH_FOR_DEBUG <= 0 || INPUTLENGTH_FOR_DEBUG == mInputLength) + && (MIN_OUTPUT_INDEX_FOR_DEBUG <= 0 + || MIN_OUTPUT_INDEX_FOR_DEBUG < mOutputIndex)) { + AKLOGI("SKIP: %d, %d, %d, %d, %c", mProximityCount, mSkippedCount, + mTransposedCount, mExcessiveCount, c); + } return processSkipChar(c, isTerminal, false); + } else if (ProximityInfo::ADDITIONAL_PROXIMITY_CHAR == matchedProximityCharId) { + // As a last resort, use additional proximity characters + mProximityMatching = true; + ++mProximityCount; + mDistances[mOutputIndex] = ADDITIONAL_PROXIMITY_CHAR_DISTANCE_INFO; + if (DEBUG_CORRECTION + && (INPUTLENGTH_FOR_DEBUG <= 0 || INPUTLENGTH_FOR_DEBUG == mInputLength) + && (MIN_OUTPUT_INDEX_FOR_DEBUG <= 0 + || MIN_OUTPUT_INDEX_FOR_DEBUG < mOutputIndex)) { + AKLOGI("ADDITIONALPROX: %d, %d, %d, %d, %c", mProximityCount, mSkippedCount, + mTransposedCount, mExcessiveCount, c); + } } else { - if (DEBUG_CORRECTION) { + if (DEBUG_CORRECTION + && (INPUTLENGTH_FOR_DEBUG <= 0 || INPUTLENGTH_FOR_DEBUG == mInputLength) + && (MIN_OUTPUT_INDEX_FOR_DEBUG <= 0 + || MIN_OUTPUT_INDEX_FOR_DEBUG < mOutputIndex)) { DUMP_WORD(mWord, mOutputIndex); AKLOGI("UNRELATED(1): %d, %d, %d, %d, %c", mProximityCount, mSkippedCount, mTransposedCount, mExcessiveCount, c); @@ -506,6 +549,13 @@ Correction::CorrectionType Correction::processCharAndCalcState( ++mProximityCount; mDistances[mOutputIndex] = mProximityInfo->getNormalizedSquaredDistance(mInputIndex, proximityIndex); + if (DEBUG_CORRECTION + && (INPUTLENGTH_FOR_DEBUG <= 0 || INPUTLENGTH_FOR_DEBUG == mInputLength) + && (MIN_OUTPUT_INDEX_FOR_DEBUG <= 0 + || MIN_OUTPUT_INDEX_FOR_DEBUG < mOutputIndex)) { + AKLOGI("PROX: %d, %d, %d, %d, %c", mProximityCount, mSkippedCount, + mTransposedCount, mExcessiveCount, c); + } } addCharToCurrentWord(c); @@ -539,7 +589,9 @@ Correction::CorrectionType Correction::processCharAndCalcState( || isSameAsUserTypedLength) && isTerminal) { mTerminalInputIndex = mInputIndex - 1; mTerminalOutputIndex = mOutputIndex - 1; - if (DEBUG_CORRECTION) { + if (DEBUG_CORRECTION + && (INPUTLENGTH_FOR_DEBUG <= 0 || INPUTLENGTH_FOR_DEBUG == mInputLength) + && (MIN_OUTPUT_INDEX_FOR_DEBUG <= 0 || MIN_OUTPUT_INDEX_FOR_DEBUG < mOutputIndex)) { DUMP_WORD(mWord, mOutputIndex); AKLOGI("ONTERMINAL(1): %d, %d, %d, %d, %c", mProximityCount, mSkippedCount, mTransposedCount, mExcessiveCount, c); @@ -678,7 +730,7 @@ int Correction::RankingAlgorithm::calculateFinalFreq(const int inputIndex, const if (excessiveCount > 0) { multiplyRate(WORDS_WITH_EXCESSIVE_CHARACTER_DEMOTION_RATE, &finalFreq); if (!lastCharExceeded && !proximityInfo->existsAdjacentProximityChars(excessivePos)) { - if (DEBUG_CORRECTION_FREQ) { + if (DEBUG_DICT_FULL) { AKLOGI("Double excessive demotion"); } // If an excessive character is not adjacent to the left char or the right char, @@ -687,51 +739,46 @@ int Correction::RankingAlgorithm::calculateFinalFreq(const int inputIndex, const } } + const bool performTouchPositionCorrection = + CALIBRATE_SCORE_BY_TOUCH_COORDINATES && proximityInfo->touchPositionCorrectionEnabled() + && skippedCount == 0 && excessiveCount == 0 && transposedCount == 0; // Score calibration by touch coordinates is being done only for pure-fat finger typing error // cases. // TODO: Remove this constraint. - if (CALIBRATE_SCORE_BY_TOUCH_COORDINATES && proximityInfo->touchPositionCorrectionEnabled() - && skippedCount == 0 && excessiveCount == 0 && transposedCount == 0) { - for (int i = 0; i < outputLength; ++i) { - const int squaredDistance = correction->mDistances[i]; - if (i < adjustedProximityMatchedCount) { - multiplyIntCapped(typedLetterMultiplier, &finalFreq); - } - if (squaredDistance >= 0) { - // Promote or demote the score according to the distance from the sweet spot - static const float A = ZERO_DISTANCE_PROMOTION_RATE / 100.0f; - static const float B = 1.0f; - static const float C = 0.5f; - static const float R1 = NEUTRAL_SCORE_SQUARED_RADIUS; - static const float R2 = HALF_SCORE_SQUARED_RADIUS; - const float x = (float)squaredDistance - / ProximityInfo::NORMALIZED_SQUARED_DISTANCE_SCALING_FACTOR; - const float factor = (x < R1) - ? (A * (R1 - x) + B * x) / R1 - : (B * (R2 - x) + C * (x - R1)) / (R2 - R1); - // factor is piecewise linear function like: - // A -_ . - // ^-_ . - // B \ . - // \ . - // C \ . - // 0 R1 R2 - if (factor <= 0) { - return -1; - } - multiplyRate((int)(factor * 100), &finalFreq); - } else if (squaredDistance == PROXIMITY_CHAR_WITHOUT_DISTANCE_INFO) { - multiplyRate(WORDS_WITH_PROXIMITY_CHARACTER_DEMOTION_RATE, &finalFreq); - } - } - } else { - // Promotion for a word with proximity characters - for (int i = 0; i < adjustedProximityMatchedCount; ++i) { - // A word with proximity corrections - if (DEBUG_DICT_FULL) { - AKLOGI("Found a proximity correction."); - } + for (int i = 0; i < outputLength; ++i) { + const int squaredDistance = correction->mDistances[i]; + if (i < adjustedProximityMatchedCount) { multiplyIntCapped(typedLetterMultiplier, &finalFreq); + } + + if (performTouchPositionCorrection && squaredDistance >= 0) { + // Promote or demote the score according to the distance from the sweet spot + static const float A = ZERO_DISTANCE_PROMOTION_RATE / 100.0f; + static const float B = 1.0f; + static const float C = 0.5f; + static const float MIN = 0.3f; + static const float R1 = NEUTRAL_SCORE_SQUARED_RADIUS; + static const float R2 = HALF_SCORE_SQUARED_RADIUS; + const float x = (float)squaredDistance + / ProximityInfo::NORMALIZED_SQUARED_DISTANCE_SCALING_FACTOR; + const float factor = max((x < R1) + ? (A * (R1 - x) + B * x) / R1 + : (B * (R2 - x) + C * (x - R1)) / (R2 - R1), MIN); + // factor is piecewise linear function like: + // A -_ . + // ^-_ . + // B \ . + // \_ . + // C ------------. + // . + // 0 R1 R2 . + multiplyRate((int)(factor * 100), &finalFreq); + } else if (performTouchPositionCorrection + && squaredDistance == PROXIMITY_CHAR_WITHOUT_DISTANCE_INFO) { + multiplyRate(WORDS_WITH_PROXIMITY_CHARACTER_DEMOTION_RATE, &finalFreq); + } else if (squaredDistance == ADDITIONAL_PROXIMITY_CHAR_DISTANCE_INFO) { + multiplyRate(WORDS_WITH_ADDITIONAL_PROXIMITY_CHARACTER_DEMOTION_RATE, &finalFreq); + } else if (i < adjustedProximityMatchedCount) { multiplyRate(WORDS_WITH_PROXIMITY_CHARACTER_DEMOTION_RATE, &finalFreq); } } @@ -794,7 +841,8 @@ int Correction::RankingAlgorithm::calculateFinalFreq(const int inputIndex, const AKLOGI("calc: %d, %d", outputLength, sameLength); } - if (DEBUG_CORRECTION_FREQ) { + if (DEBUG_CORRECTION_FREQ + && (INPUTLENGTH_FOR_DEBUG <= 0 || INPUTLENGTH_FOR_DEBUG == inputLength)) { DUMP_WORD(correction->mWord, outputLength); AKLOGI("FinalFreq: [P%d, S%d, T%d, E%d] %d, %d, %d, %d, %d, %d", proximityMatchedCount, skippedCount, transposedCount, excessiveCount, outputLength, lastCharExceeded, diff --git a/native/src/correction.h b/native/src/correction.h index b246070fe..a711c994d 100644 --- a/native/src/correction.h +++ b/native/src/correction.h @@ -85,7 +85,7 @@ class Correction { } } - Correction(const int typedLetterMultiplier, const int fullWordMultiplier); + Correction(const int typedLetterMultiplier, const int fullWordMultiplier); void initCorrection( const ProximityInfo *pi, const int inputLength, const int maxWordLength); void initCorrectionState(const int rootPos, const int childCount, const bool traverseAll); diff --git a/native/src/defines.h b/native/src/defines.h index 3f3f5ba5c..02c1fe0a2 100644 --- a/native/src/defines.h +++ b/native/src/defines.h @@ -172,6 +172,7 @@ static void prof_out(void) { #define NOT_A_COORDINATE -1 #define EQUIVALENT_CHAR_WITHOUT_DISTANCE_INFO -2 #define PROXIMITY_CHAR_WITHOUT_DISTANCE_INFO -3 +#define ADDITIONAL_PROXIMITY_CHAR_DISTANCE_INFO -4 #define NOT_A_INDEX -1 #define NOT_A_FREQUENCY -1 @@ -194,6 +195,7 @@ static void prof_out(void) { #define WORDS_WITH_TRANSPOSED_CHARACTERS_DEMOTION_RATE 70 #define FULL_MATCHED_WORDS_PROMOTION_RATE 120 #define WORDS_WITH_PROXIMITY_CHARACTER_DEMOTION_RATE 90 +#define WORDS_WITH_ADDITIONAL_PROXIMITY_CHARACTER_DEMOTION_RATE 30 #define WORDS_WITH_MATCH_SKIP_PROMOTION_RATE 105 #define WORDS_WITH_JUST_ONE_CORRECTION_PROMOTION_RATE 160 #define CORRECTION_COUNT_RATE_DEMOTION_RATE_BASE 45 @@ -210,6 +212,9 @@ static void prof_out(void) { // This is only used for the size of array. Not to be used in c functions. #define MAX_WORD_LENGTH_INTERNAL 48 +// This must be equal to ADDITIONAL_PROXIMITY_CHAR_DELIMITER_CODE in KeyDetector.java +#define ADDITIONAL_PROXIMITY_CHAR_DELIMITER_CODE 2 + // Word limit for sub queues used in WordsPriorityQueuePool. Sub queues are temporary queues used // for better performance. // Holds up to 1 candidate for each word @@ -241,4 +246,8 @@ static void prof_out(void) { // The ratio of neutral area radius to sweet spot radius. #define NEUTRAL_AREA_RADIUS_RATIO 1.3f +// DEBUG +#define INPUTLENGTH_FOR_DEBUG -1 +#define MIN_OUTPUT_INDEX_FOR_DEBUG -1 + #endif // LATINIME_DEFINES_H diff --git a/native/src/proximity_info.cpp b/native/src/proximity_info.cpp index e0e938099..b6bab2274 100644 --- a/native/src/proximity_info.cpp +++ b/native/src/proximity_info.cpp @@ -261,7 +261,8 @@ ProximityInfo::ProximityType ProximityInfo::getMatchedProximityId(const int inde // Not an exact nor an accent-alike match: search the list of close keys int j = 1; - while (j < MAX_PROXIMITY_CHARS_SIZE && currentChars[j] > 0) { + while (j < MAX_PROXIMITY_CHARS_SIZE + && currentChars[j] > ADDITIONAL_PROXIMITY_CHAR_DELIMITER_CODE) { const bool matched = (currentChars[j] == baseLowerC || currentChars[j] == c); if (matched) { if (proximityIndex) { @@ -271,6 +272,21 @@ ProximityInfo::ProximityType ProximityInfo::getMatchedProximityId(const int inde } ++j; } + if (j < MAX_PROXIMITY_CHARS_SIZE + && currentChars[j] == ADDITIONAL_PROXIMITY_CHAR_DELIMITER_CODE) { + ++j; + while (j < MAX_PROXIMITY_CHARS_SIZE + && currentChars[j] > ADDITIONAL_PROXIMITY_CHAR_DELIMITER_CODE) { + const bool matched = (currentChars[j] == baseLowerC || currentChars[j] == c); + if (matched) { + if (proximityIndex) { + *proximityIndex = j; + } + return ADDITIONAL_PROXIMITY_CHAR; + } + ++j; + } + } // Was not included, signal this as an unrelated character. return UNRELATED_CHAR; diff --git a/native/src/proximity_info.h b/native/src/proximity_info.h index 9ca5505a7..b77c1bb0a 100644 --- a/native/src/proximity_info.h +++ b/native/src/proximity_info.h @@ -38,7 +38,9 @@ class ProximityInfo { // It is a char located nearby on the keyboard NEAR_PROXIMITY_CHAR, // It is an unrelated char - UNRELATED_CHAR + UNRELATED_CHAR, + // Additional proximity char which can differ by language. + ADDITIONAL_PROXIMITY_CHAR } ProximityType; ProximityInfo(const int maxProximityCharsSize, const int keyboardWidth, diff --git a/tests/src/com/android/inputmethod/keyboard/internal/KeyStylesTests.java b/tests/src/com/android/inputmethod/keyboard/internal/CsvParserTests.java index 54a8e6246..ef80d4f0c 100644 --- a/tests/src/com/android/inputmethod/keyboard/internal/KeyStylesTests.java +++ b/tests/src/com/android/inputmethod/keyboard/internal/CsvParserTests.java @@ -20,12 +20,11 @@ import android.content.res.Resources; import android.test.AndroidTestCase; import android.text.TextUtils; -import com.android.inputmethod.latin.Utils; import com.android.inputmethod.latin.tests.R; import java.util.Arrays; -public class KeyStylesTests extends AndroidTestCase { +public class CsvParserTests extends AndroidTestCase { private Resources mTestResources; @Override @@ -40,7 +39,7 @@ public class KeyStylesTests extends AndroidTestCase { } private void assertTextArray(String message, String value, String ... expected) { - final String actual[] = Utils.parseCsvString(value, mTestResources, + final String actual[] = KeySpecParser.parseCsvString(value, mTestResources, R.string.empty_string); if (expected.length == 0) { assertNull(message, actual); @@ -64,42 +63,77 @@ public class KeyStylesTests extends AndroidTestCase { } } + // \U001d11e: MUSICAL SYMBOL G CLEF + private static final String PAIR1 = "\ud834\udd1e"; + // \U001d122: MUSICAL SYMBOL F CLEF + private static final String PAIR2 = "\ud834\udd22"; + // \U002f8a6: CJK COMPATIBILITY IDEOGRAPH-2F8A6; variant character of \u6148. + private static final String PAIR3 = "\ud87e\udca6"; + private static final String SURROGATE1 = PAIR1 + PAIR2; + private static final String SURROGATE2 = PAIR1 + PAIR2 + PAIR3; + public void testParseCsvTextZero() { assertTextArray("Empty string", ""); } public void testParseCsvTextSingle() { assertTextArray("Single char", "a", "a"); + assertTextArray("Surrogate pair", PAIR1, PAIR1); assertTextArray("Space", " ", " "); assertTextArray("Single label", "abc", "abc"); + assertTextArray("Single srrogate pairs label", SURROGATE2, SURROGATE2); assertTextArray("Spaces", " ", " "); assertTextArray("Spaces in label", "a b c", "a b c"); assertTextArray("Spaces at beginning of label", " abc", " abc"); assertTextArray("Spaces at end of label", "abc ", "abc "); assertTextArray("Label surrounded by spaces", " abc ", " abc "); + assertTextArray("Surrogate pair surrounded by space", + " " + PAIR1 + " ", + " " + PAIR1 + " "); + assertTextArray("Surrogate pair within characters", + "ab" + PAIR2 + "cd", + "ab" + PAIR2 + "cd"); + assertTextArray("Surrogate pairs within characters", + "ab" + SURROGATE1 + "cd", + "ab" + SURROGATE1 + "cd"); assertTextArray("Incomplete resource reference 1", "string", "string"); assertTextArray("Incomplete resource reference 2", "@strin", "@strin"); + assertTextArray("Incomplete resource reference 3", "@" + SURROGATE2, "@" + SURROGATE2); } public void testParseCsvTextSingleEscaped() { assertTextArray("Escaped char", "\\a", "a"); + assertTextArray("Escaped surrogate pair", "\\" + PAIR1, PAIR1); assertTextArray("Escaped comma", "\\,", ","); assertTextArray("Escaped escape", "\\\\", "\\"); assertTextArray("Escaped label", "a\\bc", "abc"); + assertTextArray("Escaped surrogate", "a\\" + PAIR1 + "c", "a" + PAIR1 + "c"); assertTextArray("Escaped label at beginning", "\\abc", "abc"); + assertTextArray("Escaped surrogate at beginning", "\\" + SURROGATE2, SURROGATE2); assertTextArray("Escaped label with comma", "a\\,c", "a,c"); + assertTextArray("Escaped surrogate with comma", PAIR1 + "\\," + PAIR2, PAIR1 + "," + PAIR2); assertTextArray("Escaped label with comma at beginning", "\\,bc", ",bc"); + assertTextArray("Escaped surrogate with comma at beginning", + "\\," + SURROGATE1, "," + SURROGATE1); assertTextArray("Escaped label with successive", "\\,\\\\bc", ",\\bc"); + assertTextArray("Escaped surrogate with successive", + "\\,\\\\" + SURROGATE1, ",\\" + SURROGATE1); assertTextArray("Escaped label with escape", "a\\\\c", "a\\c"); + assertTextArray("Escaped surrogate with escape", + PAIR1 + "\\\\" + PAIR2, PAIR1 + "\\" + PAIR2); assertTextArray("Escaped @string", "\\@string/empty_string", "@string/empty_string"); } public void testParseCsvTextMulti() { assertTextArray("Multiple chars", "a,b,c", "a", "b", "c"); + assertTextArray("Multiple surrogates", PAIR1 + "," + PAIR2 + "," + PAIR3, + PAIR1, PAIR2, PAIR3); assertTextArray("Multiple chars surrounded by spaces", " a , b , c ", " a ", " b ", " c "); assertTextArray("Multiple labels", "abc,def,ghi", "abc", "def", "ghi"); + assertTextArray("Multiple surrogated", SURROGATE1 + "," + SURROGATE2, + SURROGATE1, SURROGATE2); assertTextArray("Multiple labels surrounded by spaces", " abc , def , ghi ", " abc ", " def ", " ghi "); } diff --git a/tests/src/com/android/inputmethod/keyboard/internal/MoreKeySpecParserTests.java b/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserTests.java index bc38cc16c..d27c55cdd 100644 --- a/tests/src/com/android/inputmethod/keyboard/internal/MoreKeySpecParserTests.java +++ b/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserTests.java @@ -24,7 +24,7 @@ import com.android.inputmethod.latin.R; import java.util.Arrays; -public class MoreKeySpecParserTests extends AndroidTestCase { +public class KeySpecParserTests extends AndroidTestCase { private Resources mRes; private static final int ICON_SETTINGS_KEY = R.styleable.Keyboard_iconSettingsKey; @@ -49,16 +49,16 @@ public class MoreKeySpecParserTests extends AndroidTestCase { private void assertParser(String message, String moreKeySpec, String expectedLabel, String expectedOutputText, int expectedIcon, int expectedCode) { - String actualLabel = MoreKeySpecParser.getLabel(moreKeySpec); + String actualLabel = KeySpecParser.getLabel(moreKeySpec); assertEquals(message + ": label:", expectedLabel, actualLabel); - String actualOutputText = MoreKeySpecParser.getOutputText(moreKeySpec); + String actualOutputText = KeySpecParser.getOutputText(moreKeySpec); assertEquals(message + ": ouptputText:", expectedOutputText, actualOutputText); - int actualIcon = MoreKeySpecParser.getIconAttrId(moreKeySpec); + int actualIcon = KeySpecParser.getIconAttrId(moreKeySpec); assertEquals(message + ": icon:", expectedIcon, actualIcon); - int actualCode = MoreKeySpecParser.getCode(mRes, moreKeySpec); + int actualCode = KeySpecParser.getCode(mRes, moreKeySpec); assertEquals(message + ": codes value:", expectedCode, actualCode); } @@ -73,9 +73,22 @@ public class MoreKeySpecParserTests extends AndroidTestCase { } } + // \U001d11e: MUSICAL SYMBOL G CLEF + private static final String PAIR1 = "\ud834\udd1e"; + private static final int CODE1 = PAIR1.codePointAt(0); + // \U001d122: MUSICAL SYMBOL F CLEF + private static final String PAIR2 = "\ud834\udd22"; + private static final int CODE2 = PAIR2.codePointAt(0); + // \U002f8a6: CJK COMPATIBILITY IDEOGRAPH-2F8A6; variant character of \u6148. + private static final String PAIR3 = "\ud87e\udca6"; + private static final String SURROGATE1 = PAIR1 + PAIR2; + private static final String SURROGATE2 = PAIR1 + PAIR2 + PAIR3; + public void testSingleLetter() { assertParser("Single letter", "a", "a", null, ICON_UNDEFINED, 'a'); + assertParser("Single surrogate", PAIR1, + PAIR1, null, ICON_UNDEFINED, CODE1); assertParser("Single escaped bar", "\\|", "|", null, ICON_UNDEFINED, '|'); assertParser("Single escaped escape", "\\\\", @@ -86,20 +99,31 @@ public class MoreKeySpecParserTests extends AndroidTestCase { ",", null, ICON_UNDEFINED, ','); assertParser("Single escaped letter", "\\a", "a", null, ICON_UNDEFINED, 'a'); + assertParser("Single escaped surrogate", "\\" + PAIR2, + PAIR2, null, ICON_UNDEFINED, CODE2); assertParser("Single at", "@", "@", null, ICON_UNDEFINED, '@'); assertParser("Single escaped at", "\\@", "@", null, ICON_UNDEFINED, '@'); assertParser("Single letter with outputText", "a|abc", "a", "abc", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT); + assertParser("Single letter with surrogate outputText", "a|" + SURROGATE1, + "a", SURROGATE1, ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT); + assertParser("Single surrogate with outputText", PAIR3 + "|abc", + PAIR3, "abc", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT); assertParser("Single letter with escaped outputText", "a|a\\|c", "a", "a|c", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT); + assertParser("Single letter with escaped surrogate outputText", + "a|" + PAIR1 + "\\|" + PAIR2, + "a", PAIR1 + "|" + PAIR2, ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT); assertParser("Single letter with comma outputText", "a|a,b", "a", "a,b", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT); assertParser("Single letter with escaped comma outputText", "a|a\\,b", "a", "a,b", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT); assertParser("Single letter with outputText starts with at", "a|@bc", "a", "@bc", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT); + assertParser("Single letter with surrogate outputText starts with at", "a|@" + SURROGATE2, + "a", "@" + SURROGATE2, ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT); assertParser("Single letter with outputText contains at", "a|a@c", "a", "a@c", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT); assertParser("Single letter with escaped at outputText", "a|\\@bc", @@ -115,8 +139,13 @@ public class MoreKeySpecParserTests extends AndroidTestCase { public void testLabel() { assertParser("Simple label", "abc", "abc", "abc", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT); + assertParser("Simple surrogate label", SURROGATE1, + SURROGATE1, SURROGATE1, ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT); assertParser("Label with escaped bar", "a\\|c", "a|c", "a|c", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT); + assertParser("Surrogate label with escaped bar", PAIR1 + "\\|" + PAIR2, + PAIR1 + "|" + PAIR2, PAIR1 + "|" + PAIR2, + ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT); assertParser("Label with escaped escape", "a\\\\c", "a\\c", "a\\c", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT); assertParser("Label with comma", "a,c", @@ -125,6 +154,8 @@ public class MoreKeySpecParserTests extends AndroidTestCase { "a,c", "a,c", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT); assertParser("Label starts with at", "@bc", "@bc", "@bc", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT); + assertParser("Surrogate label starts with at", "@" + SURROGATE1, + "@" + SURROGATE1, "@" + SURROGATE1, ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT); assertParser("Label contains at", "a@c", "a@c", "a@c", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT); assertParser("Label with escaped at", "\\@bc", @@ -220,9 +251,9 @@ public class MoreKeySpecParserTests extends AndroidTestCase { null, null, ICON_SETTINGS_KEY, mCodeSettings); } - private void assertMoreKeys(String message, String[] moreKeys, String[] additionalMoreKeys, - String[] expected) { - final String[] actual = MoreKeySpecParser.insertAddtionalMoreKeys( + private static void assertMoreKeys(String message, String[] moreKeys, + String[] additionalMoreKeys, String[] expected) { + final String[] actual = KeySpecParser.insertAddtionalMoreKeys( moreKeys, additionalMoreKeys); if (expected == null && actual == null) { return; |