Support additional proximity characters

Change-Id: Ifbe0d7e4eafea1926bbce968eae4724dd5769689
diff --git a/java/res/values-en/additional-proximitychars.xml b/java/res/values-en/additional-proximitychars.xml
new file mode 100644
index 0000000..0e12767
--- /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 0000000..03d10d5
--- /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/KeyDetector.java b/java/src/com/android/inputmethod/keyboard/KeyDetector.java
index 0d27162..bff491f 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyDetector.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyDetector.java
@@ -19,12 +19,14 @@
 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 @@
         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 @@
         }
 
         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 @@
                 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 @@
         }
 
         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 6653dec..10e0a5b 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.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.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 @@
 
     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 @@
         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 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 @@
         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 @@
             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 @@
             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/ProximityInfo.java b/java/src/com/android/inputmethod/keyboard/ProximityInfo.java
index c1dae06..2d1a008 100644
--- a/java/src/com/android/inputmethod/keyboard/ProximityInfo.java
+++ b/java/src/com/android/inputmethod/keyboard/ProximityInfo.java
@@ -24,6 +24,9 @@
 
 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 @@
     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 @@
             // 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 @@
     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 @@
                     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 @@
     }
 
     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 @@
                         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 @@
             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/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java
index 230c291..bd244b9 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 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;