Merge "Move MoreKeySpecParser.getResourceId to Utils"
diff --git a/java/src/com/android/inputmethod/keyboard/Key.java b/java/src/com/android/inputmethod/keyboard/Key.java
index ebd6150..6003d97 100644
--- a/java/src/com/android/inputmethod/keyboard/Key.java
+++ b/java/src/com/android/inputmethod/keyboard/Key.java
@@ -101,7 +101,7 @@
     /** Text to output when pressed. This can be multiple characters, like ".com" */
     public final CharSequence mOutputText;
     /** More keys */
-    public final CharSequence[] mMoreKeys;
+    public final String[] mMoreKeys;
     /** More keys maximum column number */
     public final int mMaxMoreKeysColumn;
 
@@ -255,7 +255,7 @@
         // Update row to have current x coordinate.
         row.setXPos(keyXPos + keyWidth);
 
-        final CharSequence[] moreKeys = style.getTextArray(keyAttr,
+        final String[] moreKeys = style.getTextArray(keyAttr,
                 R.styleable.Keyboard_Key_moreKeys);
         // In Arabic symbol layouts, we'd like to keep digits in more keys regardless of
         // config_digit_more_keys_enabled.
diff --git a/java/src/com/android/inputmethod/keyboard/MiniKeyboard.java b/java/src/com/android/inputmethod/keyboard/MiniKeyboard.java
index 548b5ea..9742913 100644
--- a/java/src/com/android/inputmethod/keyboard/MiniKeyboard.java
+++ b/java/src/com/android/inputmethod/keyboard/MiniKeyboard.java
@@ -34,7 +34,7 @@
     }
 
     public static class Builder extends Keyboard.Builder<Builder.MiniKeyboardParams> {
-        private final CharSequence[] mMoreKeys;
+        private final String[] mMoreKeys;
 
         public static class MiniKeyboardParams extends Keyboard.Params {
             /* package */int mTopRowAdjustment;
@@ -230,16 +230,14 @@
                     parentKey.mX + (mParams.mDefaultKeyWidth - width) / 2, view.getMeasuredWidth());
         }
 
-        private static int getMaxKeyWidth(KeyboardView view, CharSequence[] moreKeys,
-                int minKeyWidth) {
+        private static int getMaxKeyWidth(KeyboardView view, String[] moreKeys, int minKeyWidth) {
             final int padding = (int) view.getContext().getResources()
                     .getDimension(R.dimen.mini_keyboard_key_horizontal_padding);
             Paint paint = null;
             int maxWidth = minKeyWidth;
-            for (CharSequence moreKeySpec : moreKeys) {
-                final CharSequence label = MoreKeySpecParser.getLabel(moreKeySpec.toString());
-                // If the label is single letter, minKeyWidth is enough to hold
-                // the label.
+            for (String moreKeySpec : moreKeys) {
+                final String label = MoreKeySpecParser.getLabel(moreKeySpec);
+                // If the label is single letter, minKeyWidth is enough to hold the label.
                 if (label != null && label.length() > 1) {
                     if (paint == null) {
                         paint = new Paint();
@@ -258,7 +256,7 @@
         public MiniKeyboard build() {
             final MiniKeyboardParams params = mParams;
             for (int n = 0; n < mMoreKeys.length; n++) {
-                final String moreKeySpec = mMoreKeys[n].toString();
+                final String moreKeySpec = mMoreKeys[n];
                 final int row = n / params.mNumColumns;
                 final Key key = new Key(mResources, params, moreKeySpec, params.getX(n, row),
                         params.getY(row), params.mDefaultKeyWidth, params.mDefaultRowHeight);
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyStyles.java b/java/src/com/android/inputmethod/keyboard/internal/KeyStyles.java
index 5dd8340..0f84c45 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyStyles.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyStyles.java
@@ -37,20 +37,22 @@
             new HashMap<String, DeclaredKeyStyle>();
     private static final KeyStyle EMPTY_KEY_STYLE = new EmptyKeyStyle();
 
+    private static final char ESCAPE_CHAR = '\\';
+
     public interface KeyStyle {
-        public CharSequence[] getTextArray(TypedArray a, int index);
+        public String[] getTextArray(TypedArray a, int index);
         public CharSequence getText(TypedArray a, int index);
         public int getInt(TypedArray a, int index, int defaultValue);
         public int getFlag(TypedArray a, int index, int defaultValue);
     }
 
-    /* package */ static class EmptyKeyStyle implements KeyStyle {
+    private static class EmptyKeyStyle implements KeyStyle {
         EmptyKeyStyle() {
             // Nothing to do.
         }
 
         @Override
-        public CharSequence[] getTextArray(TypedArray a, int index) {
+        public String[] getTextArray(TypedArray a, int index) {
             return parseTextArray(a, index);
         }
 
@@ -69,64 +71,66 @@
             return a.getInt(index, defaultValue);
         }
 
-        protected static CharSequence[] parseTextArray(TypedArray a, int index) {
+        protected static String[] parseTextArray(TypedArray a, int index) {
             if (!a.hasValue(index))
                 return null;
             final CharSequence text = a.getText(index);
-            return parseCsvText(text);
+            return parseCsvText(text.toString());
         }
 
-        /* package */ static CharSequence[] parseCsvText(CharSequence text) {
-            final int size = text.length();
-            if (size == 0) return null;
-            if (size == 1) return new CharSequence[] { text };
-            final StringBuilder sb = new StringBuilder();
-            ArrayList<CharSequence> 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<CharSequence>();
-                    if (sb.length() == 0) {
-                        list.add(text.subSequence(start, pos));
-                    } else {
-                        list.add(sb.toString());
-                        sb.setLength(0);
-                    }
-                    start = pos + 1;
-                    continue;
-                } else if (c == '\\') {
-                    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);
+    }
+
+    /* package for test */
+    static String[] parseCsvText(String text) {
+        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 CharSequence[] { sb.length() > 0 ? sb : text.subSequence(start, size) };
-            } else {
-                list.add(sb.length() > 0 ? sb : text.subSequence(start, size));
-                return list.toArray(new CharSequence[list.size()]);
-            }
+        }
+        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()]);
         }
     }
 
-    /* package */ static class DeclaredKeyStyle extends EmptyKeyStyle {
+    private static class DeclaredKeyStyle extends EmptyKeyStyle {
         private final HashMap<Integer, Object> mAttributes = new HashMap<Integer, Object>();
 
         @Override
-        public CharSequence[] getTextArray(TypedArray a, int index) {
+        public String[] getTextArray(TypedArray a, int index) {
             return a.hasValue(index)
-                    ? super.getTextArray(a, index) : (CharSequence[])mAttributes.get(index);
+                    ? super.getTextArray(a, index) : (String[])mAttributes.get(index);
         }
 
         @Override
diff --git a/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpecParser.java b/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpecParser.java
index d29a9e5..d4c85c3 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpecParser.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpecParser.java
@@ -41,7 +41,7 @@
 public class MoreKeySpecParser {
     private static final String TAG = MoreKeySpecParser.class.getSimpleName();
 
-    private static final char ESCAPE = '\\';
+    private static final char ESCAPE_CHAR = '\\';
     private static final String LABEL_END = "|";
     private static final String PREFIX_AT = "@";
     private static final String PREFIX_ICON = PREFIX_AT + "icon/";
@@ -72,14 +72,14 @@
     }
 
     private static String parseEscape(String text) {
-        if (text.indexOf(ESCAPE) < 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 == ESCAPE && pos + 1 < length) {
+            if (c == ESCAPE_CHAR && pos + 1 < length) {
                 sb.append(text.charAt(++pos));
             } else {
                 sb.append(c);
@@ -89,7 +89,7 @@
     }
 
     private static int indexOfLabelEnd(String moreKeySpec, int start) {
-        if (moreKeySpec.indexOf(ESCAPE, 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);
@@ -99,7 +99,7 @@
         final int length = moreKeySpec.length();
         for (int pos = start; pos < length; pos++) {
             final char c = moreKeySpec.charAt(pos);
-            if (c == ESCAPE && pos + 1 < length) {
+            if (c == ESCAPE_CHAR && pos + 1 < length) {
                 pos++;
             } else if (moreKeySpec.startsWith(LABEL_END, pos)) {
                 return pos;
@@ -200,21 +200,19 @@
         }
     };
 
-    public static CharSequence[] filterOut(Resources res, CharSequence[] moreKeys,
-            CodeFilter filter) {
+    public static String[] filterOut(Resources res, String[] moreKeys, CodeFilter filter) {
         if (moreKeys == null || moreKeys.length < 1) {
             return null;
         }
-        if (moreKeys.length == 1
-                && filter.shouldFilterOut(getCode(res, moreKeys[0].toString()))) {
+        if (moreKeys.length == 1 && filter.shouldFilterOut(getCode(res, moreKeys[0]))) {
             return null;
         }
-        ArrayList<CharSequence> filtered = null;
+        ArrayList<String> filtered = null;
         for (int i = 0; i < moreKeys.length; i++) {
-            final CharSequence moreKeySpec = moreKeys[i];
-            if (filter.shouldFilterOut(getCode(res, moreKeySpec.toString()))) {
+            final String moreKeySpec = moreKeys[i];
+            if (filter.shouldFilterOut(getCode(res, moreKeySpec))) {
                 if (filtered == null) {
-                    filtered = new ArrayList<CharSequence>();
+                    filtered = new ArrayList<String>();
                     for (int j = 0; j < i; j++) {
                         filtered.add(moreKeys[j]);
                     }
@@ -229,6 +227,6 @@
         if (filtered.size() == 0) {
             return null;
         }
-        return filtered.toArray(new CharSequence[filtered.size()]);
+        return filtered.toArray(new String[filtered.size()]);
     }
 }
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index d36140d..78e6602 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -1881,6 +1881,10 @@
             }
             return;
         }
+        // We need to log before we commit, because the word composer will store away the user
+        // typed word.
+        LatinImeLogger.logOnManualSuggestion(mWordComposer.getTypedWord().toString(),
+                suggestion.toString(), index, suggestions.mWords);
         mExpectingUpdateSelection = true;
         commitChosenWord(suggestion, WordComposer.COMMIT_TYPE_MANUAL_PICK);
         // Add the word to the auto dictionary if it's not a known word
@@ -1890,10 +1894,6 @@
         } else {
             addToOnlyBigramDictionary(suggestion, 1);
         }
-        // TODO: the following is fishy, because it seems there may be cases where we are not
-        // composing a word at all. Maybe throw an exception if !mWordComposer.isComposingWord() ?
-        LatinImeLogger.logOnManualSuggestion(mWordComposer.getTypedWord().toString(),
-                suggestion.toString(), index, suggestions.mWords);
         // Follow it with a space
         if (mInputAttributes.mInsertSpaceOnPickSuggestionManually) {
             sendMagicSpace();
diff --git a/tests/src/com/android/inputmethod/keyboard/internal/KeyStylesTests.java b/tests/src/com/android/inputmethod/keyboard/internal/KeyStylesTests.java
index 4050a71..cebe409 100644
--- a/tests/src/com/android/inputmethod/keyboard/internal/KeyStylesTests.java
+++ b/tests/src/com/android/inputmethod/keyboard/internal/KeyStylesTests.java
@@ -16,8 +16,6 @@
 
 package com.android.inputmethod.keyboard.internal;
 
-import com.android.inputmethod.keyboard.internal.KeyStyles.EmptyKeyStyle;
-
 import android.test.AndroidTestCase;
 import android.text.TextUtils;
 
@@ -26,9 +24,8 @@
         return message + " expected:<" + expected + "> but was:<" + actual + ">";
     }
 
-    private static void assertTextArray(String message, CharSequence value,
-            CharSequence ... expected) {
-        final CharSequence actual[] = EmptyKeyStyle.parseCsvText(value);
+    private static void assertTextArray(String message, String value, String ... expected) {
+        final String actual[] = KeyStyles.parseCsvText(value);
         if (expected.length == 0) {
             assertNull(message, actual);
             return;
diff --git a/tests/src/com/android/inputmethod/latin/InputLogicTests.java b/tests/src/com/android/inputmethod/latin/InputLogicTests.java
new file mode 100644
index 0000000..18afe11
--- /dev/null
+++ b/tests/src/com/android/inputmethod/latin/InputLogicTests.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.inputmethod.latin;
+
+import android.content.Context;
+import android.content.Intent;
+import android.test.ServiceTestCase;
+import android.text.InputType;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+import android.view.View;
+import android.view.inputmethod.BaseInputConnection;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.keyboard.KeyboardActionListener;
+
+public class InputLogicTests extends ServiceTestCase<LatinIME> {
+
+    private LatinIME mLatinIME;
+    private TextView mTextView;
+
+    public InputLogicTests() {
+        super(LatinIME.class);
+    }
+
+    @Override
+    protected void setUp() {
+        try {
+            super.setUp();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        mTextView = new TextView(getContext());
+        mTextView.setInputType(InputType.TYPE_CLASS_TEXT);
+        mTextView.setEnabled(true);
+        setupService();
+        mLatinIME = getService();
+        mLatinIME.onCreate();
+        final EditorInfo ei = new EditorInfo();
+        final InputConnection ic = mTextView.onCreateInputConnection(ei);
+        final LayoutInflater inflater =
+                (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        final ViewGroup vg = new FrameLayout(getContext());
+        final View inputView = inflater.inflate(R.layout.input_view, vg);
+        mLatinIME.setInputView(inputView);
+        mLatinIME.onBindInput();
+        mLatinIME.onCreateInputView();
+        mLatinIME.onStartInputView(ei, false);
+        mLatinIME.onCreateInputMethodInterface().startInput(ic, ei);
+    }
+
+    // type(int) and type(String): helper methods to send a code point resp. a string to LatinIME.
+    private void type(final int codePoint) {
+        // onPressKey and onReleaseKey are explicitly deactivated here, but they do happen in the
+        // code (although multitouch/slide input and other factors make the sequencing complicated).
+        // They are supposed to be entirely deconnected from the input logic from LatinIME point of
+        // view and only delegates to the parts of the code that care. So we don't include them here
+        // to keep these tests as pinpoint as possible and avoid bringing it too many dependencies,
+        // but keep them in mind if something breaks. Commenting them out as is should work.
+        //mLatinIME.onPressKey(codePoint);
+        mLatinIME.onCodeInput(codePoint, new int[] { codePoint },
+                KeyboardActionListener.NOT_A_TOUCH_COORDINATE,
+                KeyboardActionListener.NOT_A_TOUCH_COORDINATE);
+        //mLatinIME.onReleaseKey(codePoint, false);
+    }
+
+    private void type(final String stringToType) {
+        for (int i = 0; i < stringToType.length(); ++i) {
+            type(stringToType.codePointAt(i));
+        }
+    }
+
+    public void testTypeWord() {
+        final String wordToType = "abcd";
+        type(wordToType);
+        assertEquals("type word", wordToType, mTextView.getText().toString());
+    }
+
+    public void testPickSuggestionThenBackspace() {
+        final String wordToType = "tgis";
+        type(wordToType);
+        mLatinIME.pickSuggestionManually(0, wordToType);
+        type(Keyboard.CODE_DELETE);
+        assertEquals("press suggestion then backspace", wordToType, mTextView.getText().toString());
+    }
+
+}