Add Key preserveCase enum to keyLabelOptions attribute

To support auto generate key depending keyboard element id, the
KeysCache class is introduced to hold whole keys and reuse.

Change-Id: Icb81b5f1c1b3aaa31968dcdb93aa0a856e737f78
diff --git a/java/res/values/attrs.xml b/java/res/values/attrs.xml
index 5824d17..70fc7f8 100644
--- a/java/res/values/attrs.xml
+++ b/java/res/values/attrs.xml
@@ -242,6 +242,9 @@
             <flag name="withIconLeft" value="0x1000" />
             <flag name="withIconRight" value="0x2000" />
             <flag name="autoXScale" value="0x4000" />
+            <!-- If true, character case of code, altCode, moreKeys, keyOutputText, keyLabel,
+                 or keyHintLabel will never be subject to change. -->
+            <flag name="preserveCase" value="0x8000" />
         </attr>
         <!-- The icon to display on the key instead of the label. -->
         <attr name="keyIcon" format="enum">
diff --git a/java/res/xml/keyboard_set.xml b/java/res/xml/keyboard_set.xml
index 03eb778..27ef316 100644
--- a/java/res/xml/keyboard_set.xml
+++ b/java/res/xml/keyboard_set.xml
@@ -23,7 +23,8 @@
     latin:keyboardLocale="en_GB,en_US">
     <Element
         latin:elementName="alphabet"
-        latin:elementKeyboard="@xml/kbd_qwerty" />
+        latin:elementKeyboard="@xml/kbd_qwerty"
+        latin:elementAutoGenerate="true" />
     <Element
         latin:elementName="alphabetManualShifted"
         latin:elementKeyboard="@xml/kbd_qwerty"
diff --git a/java/src/com/android/inputmethod/keyboard/Key.java b/java/src/com/android/inputmethod/keyboard/Key.java
index caaed7e..9c495fd 100644
--- a/java/src/com/android/inputmethod/keyboard/Key.java
+++ b/java/src/com/android/inputmethod/keyboard/Key.java
@@ -71,6 +71,7 @@
     private static final int LABEL_FLAGS_WITH_ICON_LEFT = 0x1000;
     private static final int LABEL_FLAGS_WITH_ICON_RIGHT = 0x2000;
     private static final int LABEL_FLAGS_AUTO_X_SCALE = 0x4000;
+    private static final int LABEL_FLAGS_PRESERVE_CASE = 0x8000;
 
     /** Icon to display instead of a label. Icon takes precedence over a label */
     private final int mIconAttrId;
@@ -262,19 +263,6 @@
         // Update row to have current x coordinate.
         row.setXPos(keyXPos + keyWidth);
 
-        final String[] moreKeys = style.getStringArray(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.
-        if (params.mId.isAlphabetKeyboard()
-                && !res.getBoolean(R.bool.config_digit_more_keys_enabled)) {
-            mMoreKeys = MoreKeySpecParser.filterOut(res, moreKeys, MoreKeySpecParser.DIGIT_FILTER);
-        } else {
-            mMoreKeys = moreKeys;
-        }
-        mMaxMoreKeysColumn = style.getInt(keyAttr,
-                R.styleable.Keyboard_Key_maxMoreKeysColumn, params.mMaxMiniKeyboardColumn);
-
         mBackgroundType = style.getInt(keyAttr,
                 R.styleable.Keyboard_Key_backgroundType, BACKGROUND_TYPE_NORMAL);
         mActionFlags = style.getFlag(keyAttr, R.styleable.Keyboard_Key_keyActionFlags, 0);
@@ -292,14 +280,39 @@
         mDisabledIconAttrId = KeyboardIconsSet.getIconAttrId(style.getInt(keyAttr,
                 R.styleable.Keyboard_Key_keyIconDisabled, KeyboardIconsSet.ICON_UNDEFINED));
 
-        mLabel = style.getString(keyAttr, R.styleable.Keyboard_Key_keyLabel);
-        mHintLabel = style.getString(keyAttr, R.styleable.Keyboard_Key_keyHintLabel);
         mLabelFlags = style.getFlag(keyAttr, R.styleable.Keyboard_Key_keyLabelFlags, 0);
-        mOutputText = style.getString(keyAttr, R.styleable.Keyboard_Key_keyOutputText);
+        final boolean preserveCase = (mLabelFlags & LABEL_FLAGS_PRESERVE_CASE) != 0;
+
+        final String[] moreKeys = style.getStringArray(keyAttr, R.styleable.Keyboard_Key_moreKeys);
+        if (moreKeys != null) {
+            for (int i = 0; i < moreKeys.length; i++) {
+                moreKeys[i] = adjustCaseOfStringForKeyboardId(
+                        moreKeys[i], preserveCase, params.mId);
+            }
+        }
+        // TODO: Add new key label flag to control this.
+        // In Arabic symbol layouts, we'd like to keep digits in more keys regardless of
+        // config_digit_more_keys_enabled.
+        if (params.mId.isAlphabetKeyboard()
+                && !res.getBoolean(R.bool.config_digit_more_keys_enabled)) {
+            mMoreKeys = MoreKeySpecParser.filterOut(res, moreKeys, MoreKeySpecParser.DIGIT_FILTER);
+        } else {
+            mMoreKeys = moreKeys;
+        }
+        mMaxMoreKeysColumn = style.getInt(keyAttr,
+                R.styleable.Keyboard_Key_maxMoreKeysColumn, params.mMaxMiniKeyboardColumn);
+
+        mLabel = adjustCaseOfStringForKeyboardId(style.getString(
+                keyAttr, R.styleable.Keyboard_Key_keyLabel), preserveCase, params.mId);
+        mHintLabel = adjustCaseOfStringForKeyboardId(style.getString(
+                keyAttr, R.styleable.Keyboard_Key_keyHintLabel), preserveCase, params.mId);
+        mOutputText = adjustCaseOfStringForKeyboardId(style.getString(
+                keyAttr, R.styleable.Keyboard_Key_keyOutputText), preserveCase, params.mId);
         // Choose the first letter of the label as primary code if not
         // specified.
-        final int code = style.getInt(keyAttr, R.styleable.Keyboard_Key_code,
-                Keyboard.CODE_UNSPECIFIED);
+        final int code = adjustCaseOfCodeForKeyboardId(style.getInt(
+                keyAttr, R.styleable.Keyboard_Key_code, Keyboard.CODE_UNSPECIFIED), preserveCase,
+                params.mId);
         if (code == Keyboard.CODE_UNSPECIFIED && mOutputText == null
                 && !TextUtils.isEmpty(mLabel)) {
             if (mLabel.length() != 1) {
@@ -312,13 +325,36 @@
         } else {
             mCode = code;
         }
-        mAltCode = style.getInt(keyAttr,
-                R.styleable.Keyboard_Key_altCode, Keyboard.CODE_UNSPECIFIED);
+        mAltCode = adjustCaseOfCodeForKeyboardId(style.getInt(keyAttr,
+                R.styleable.Keyboard_Key_altCode, Keyboard.CODE_UNSPECIFIED), preserveCase,
+                params.mId);
         mHashCode = hashCode(this);
 
         keyAttr.recycle();
     }
 
+    private static int adjustCaseOfCodeForKeyboardId(int code, boolean preserveCase,
+            KeyboardId id) {
+        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.codePointAt(0);
+    }
+
+    private static String adjustCaseOfStringForKeyboardId(String text, boolean preserveCase,
+            KeyboardId id) {
+        if (text == null || preserveCase) return text;
+        switch (id.mElementId) {
+        case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED:
+        case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED:
+        case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED:
+        case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED:
+            return text.toUpperCase(id.mLocale);
+        default:
+            return text;
+        }
+    }
+
     private static int hashCode(Key key) {
         return Arrays.hashCode(new Object[] {
                 key.mX,
diff --git a/java/src/com/android/inputmethod/keyboard/Keyboard.java b/java/src/com/android/inputmethod/keyboard/Keyboard.java
index 72fc338..abe5b62 100644
--- a/java/src/com/android/inputmethod/keyboard/Keyboard.java
+++ b/java/src/com/android/inputmethod/keyboard/Keyboard.java
@@ -293,6 +293,8 @@
         public final Set<Key> mShiftLockKeys = new HashSet<Key>();
         public final KeyboardIconsSet mIconsSet = new KeyboardIconsSet();
 
+        public KeyboardSet.KeysCache mKeysCache;
+
         public int mMostCommonKeyHeight = 0;
         public int mMostCommonKeyWidth = 0;
 
@@ -361,7 +363,8 @@
             clearHistogram();
         }
 
-        public void onAddKey(Key key) {
+        public void onAddKey(Key newKey) {
+            final Key key = (mKeysCache != null) ? mKeysCache.get(newKey) : newKey;
             mKeys.add(key);
             updateHistogram(key);
             if (key.mCode == Keyboard.CODE_SHIFT) {
@@ -688,6 +691,10 @@
             params.mTouchPositionCorrection.load(data);
         }
 
+        public void setAutoGenerate(KeyboardSet.KeysCache keysCache) {
+            mParams.mKeysCache = keysCache;
+        }
+
         public Builder<KP> load(int xmlId, KeyboardId id) {
             mParams.mId = id;
             final XmlResourceParser parser = mResources.getXml(xmlId);
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardSet.java b/java/src/com/android/inputmethod/keyboard/KeyboardSet.java
index c7f964a..cacb8a3 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardSet.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardSet.java
@@ -57,6 +57,25 @@
 
     private final Context mContext;
     private final Params mParams;
+    private final KeysCache mKeysCache = new KeysCache();
+
+    public static class KeysCache {
+        private final Map<Key, Key> mMap;
+
+        public KeysCache() {
+            mMap = new HashMap<Key, Key>();
+        }
+
+        public Key get(Key key) {
+            final Key existingKey = mMap.get(key);
+            if (existingKey != null) {
+                // Reuse the existing element that equals to "key" without adding "key" to the map.
+                return existingKey;
+            }
+            mMap.put(key, key);
+            return key;
+        }
+    }
 
     static class KeyboardElement {
         final int mElementId;
@@ -99,15 +118,15 @@
     }
 
     public Keyboard getMainKeyboard() {
-        return getKeyboard(false, false);
+        return getKeyboard(false, false, false);
     }
 
     public Keyboard getSymbolsKeyboard() {
-        return getKeyboard(true, false);
+        return getKeyboard(true, false, false);
     }
 
     public Keyboard getSymbolsShiftedKeyboard() {
-        final Keyboard keyboard = getKeyboard(true, true);
+        final Keyboard keyboard = getKeyboard(true, false, true);
         // TODO: Remove this logic once we introduce initial keyboard shift state attribute.
         // Symbol shift keyboard may have a shift key that has a caps lock style indicator (a.k.a.
         // sticky shift key). To show or dismiss the indicator, we need to call setShiftLocked()
@@ -116,22 +135,23 @@
         return keyboard;
     }
 
-    private Keyboard getKeyboard(boolean isSymbols, boolean isShift) {
-        final int elementId = KeyboardSet.getElementId(mParams.mMode, isSymbols, isShift);
+    private Keyboard getKeyboard(boolean isSymbols, boolean isShiftLock, boolean isShift) {
+        final int elementId = KeyboardSet.getElementId(
+                mParams.mMode, isSymbols, isShiftLock, isShift);
         final KeyboardElement keyboardElement = mParams.mElementKeyboards.get(elementId);
         // TODO: If keyboardElement.mAutoGenerate is true, the keyboard will be auto generated
         // based on keyboardElement.mKayoutId Keyboard XML definition.
         final KeyboardId id = KeyboardSet.getKeyboardId(elementId, isSymbols, mParams);
-        final Keyboard keyboard = getKeyboard(mContext, keyboardElement.mLayoutId, id);
+        final Keyboard keyboard = getKeyboard(mContext, keyboardElement, id);
         return keyboard;
     }
 
     public KeyboardId getMainKeyboardId() {
-        final int elementId = KeyboardSet.getElementId(mParams.mMode, false, false);
+        final int elementId = KeyboardSet.getElementId(mParams.mMode, false, false, false);
         return KeyboardSet.getKeyboardId(elementId, false, mParams);
     }
 
-    private Keyboard getKeyboard(Context context, int xmlId, KeyboardId id) {
+    private Keyboard getKeyboard(Context context, KeyboardElement element, KeyboardId id) {
         final Resources res = context.getResources();
         final SoftReference<Keyboard> ref = sKeyboardCache.get(id);
         Keyboard keyboard = (ref == null) ? null : ref.get();
@@ -140,7 +160,10 @@
             try {
                 final Keyboard.Builder<Keyboard.Params> builder =
                         new Keyboard.Builder<Keyboard.Params>(context, new Keyboard.Params());
-                builder.load(xmlId, id);
+                if (element.mAutoGenerate) {
+                    builder.setAutoGenerate(mKeysCache);
+                }
+                builder.load(element.mLayoutId, id);
                 builder.setTouchPositionCorrectionEnabled(mParams.mTouchPositionCorrectionEnabled);
                 keyboard = builder.build();
             } finally {
@@ -162,7 +185,8 @@
         return keyboard;
     }
 
-    private static int getElementId(int mode, boolean isSymbols, boolean isShift) {
+    private static int getElementId(int mode, boolean isSymbols, boolean isShiftLock,
+            boolean isShift) {
         switch (mode) {
         case KeyboardId.MODE_PHONE:
             return (isSymbols && isShift)
@@ -174,6 +198,7 @@
                 return isShift
                         ? KeyboardId.ELEMENT_SYMBOLS_SHIFTED : KeyboardId.ELEMENT_SYMBOLS;
             }
+            // TODO: Consult isShiftLock and isShift to determine the element.
             return KeyboardId.ELEMENT_ALPHABET;
         }
     }