Add allowRedundantMoreKeys attribute

This CL also adds a couple of custom layout tests of Nordic languages.

Bug: 10787354
Change-Id: I5e875d3f30863395511afa82f0a02deb093d3a6f
diff --git a/java/res/values/attrs.xml b/java/res/values/attrs.xml
index 8ee859b..f2072fd 100644
--- a/java/res/values/attrs.xml
+++ b/java/res/values/attrs.xml
@@ -518,6 +518,8 @@
         <attr name="enableProximityCharsCorrection" format="boolean" />
         <!-- Indicates if the keyboard layout supports being split or not. false by default -->
         <attr name="supportsSplitLayout" format="boolean" />
+        <!-- Allow redundant more keys when they are in the base layout. true by default. -->
+        <attr name="allowRedundantMoreKeys" format="boolean" />
     </declare-styleable>
 
     <declare-styleable name="KeyboardLayoutSet_Feature">
diff --git a/java/res/xml/keyboard_layout_set_nordic.xml b/java/res/xml/keyboard_layout_set_nordic.xml
index 1f00f44..d07f78a 100644
--- a/java/res/xml/keyboard_layout_set_nordic.xml
+++ b/java/res/xml/keyboard_layout_set_nordic.xml
@@ -23,7 +23,8 @@
     <Element
         latin:elementName="alphabet"
         latin:elementKeyboard="@xml/kbd_nordic"
-        latin:enableProximityCharsCorrection="true" />
+        latin:enableProximityCharsCorrection="true"
+        latin:allowRedundantMoreKeys="true" />
     <Element
         latin:elementName="symbols"
         latin:elementKeyboard="@xml/kbd_symbols" />
diff --git a/java/res/xml/keyboard_layout_set_swiss.xml b/java/res/xml/keyboard_layout_set_swiss.xml
index e17a5ab..f925b83 100644
--- a/java/res/xml/keyboard_layout_set_swiss.xml
+++ b/java/res/xml/keyboard_layout_set_swiss.xml
@@ -23,7 +23,8 @@
     <Element
         latin:elementName="alphabet"
         latin:elementKeyboard="@xml/kbd_swiss"
-        latin:enableProximityCharsCorrection="true" />
+        latin:enableProximityCharsCorrection="true"
+        latin:allowRedundantMoreKeys="true" />
     <Element
         latin:elementName="symbols"
         latin:elementKeyboard="@xml/kbd_symbols" />
diff --git a/java/src/com/android/inputmethod/compat/CharacterCompat.java b/java/src/com/android/inputmethod/compat/CharacterCompat.java
new file mode 100644
index 0000000..609fe16
--- /dev/null
+++ b/java/src/com/android/inputmethod/compat/CharacterCompat.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2014 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.compat;
+
+import java.lang.reflect.Method;
+
+public final class CharacterCompat {
+    // Note that Character.isAlphabetic(int), has been introduced in API level 19
+    // (Build.VERSION_CODE.KITKAT).
+    private static final Method METHOD_isAlphabetic = CompatUtils.getMethod(
+            Character.class, "isAlphabetic", int.class);
+
+    private CharacterCompat() {
+        // This utility class is not publicly instantiable.
+    }
+
+    public static boolean isAlphabetic(final int code) {
+        if (METHOD_isAlphabetic != null) {
+            return (Boolean)CompatUtils.invoke(null, false, METHOD_isAlphabetic, code);
+        }
+        switch (Character.getType(code)) {
+        case Character.UPPERCASE_LETTER:
+        case Character.LOWERCASE_LETTER:
+        case Character.TITLECASE_LETTER:
+        case Character.MODIFIER_LETTER:
+        case Character.OTHER_LETTER:
+        case Character.LETTER_NUMBER:
+            return true;
+        default:
+            return false;
+        }
+    }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/Key.java b/java/src/com/android/inputmethod/keyboard/Key.java
index 863a8b7..bf29b5f 100644
--- a/java/src/com/android/inputmethod/keyboard/Key.java
+++ b/java/src/com/android/inputmethod/keyboard/Key.java
@@ -395,6 +395,10 @@
      * @param key the original key.
      */
     protected Key(final Key key) {
+        this(key, key.mMoreKeys);
+    }
+
+    private Key(final Key key, final MoreKeySpec[] moreKeys) {
         // Final attributes.
         mCode = key.mCode;
         mLabel = key.mLabel;
@@ -408,7 +412,7 @@
         mX = key.mX;
         mY = key.mY;
         mHitBox.set(key.mHitBox);
-        mMoreKeys = key.mMoreKeys;
+        mMoreKeys = moreKeys;
         mMoreKeysColumnAndFlags = key.mMoreKeysColumnAndFlags;
         mBackgroundType = key.mBackgroundType;
         mActionFlags = key.mActionFlags;
@@ -420,6 +424,14 @@
         mEnabled = key.mEnabled;
     }
 
+    public static Key removeRedundantMoreKeys(final Key key,
+            final MoreKeySpec.LettersOnBaseLayout lettersOnBaseLayout) {
+        final MoreKeySpec[] moreKeys = key.getMoreKeys();
+        final MoreKeySpec[] filteredMoreKeys = MoreKeySpec.removeRedundantMoreKeys(
+                moreKeys, lettersOnBaseLayout);
+        return (filteredMoreKeys == moreKeys) ? key : new Key(key, filteredMoreKeys);
+    }
+
     private static boolean needsToUpperCase(final int labelFlags, final int keyboardElementId) {
         if ((labelFlags & LABEL_FLAGS_PRESERVE_CASE) != 0) return false;
         switch (keyboardElementId) {
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java
index 47fb7b3..52b9284 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java
@@ -97,6 +97,7 @@
         int mKeyboardXmlId;
         boolean mProximityCharsCorrectionEnabled;
         boolean mSupportsSplitLayout;
+        boolean mAllowRedundantMoreKeys;
         public ElementParams() {}
     }
 
@@ -202,6 +203,7 @@
         if (id.isAlphabetKeyboard()) {
             builder.setAutoGenerate(sKeysCache);
         }
+        builder.setAllowRedundantMoreKes(elementParams.mAllowRedundantMoreKeys);
         final int keyboardXmlId = elementParams.mKeyboardXmlId;
         builder.load(keyboardXmlId, id);
         if (mParams.mDisableTouchPositionCorrectionDataForTest) {
@@ -395,6 +397,8 @@
                         false);
                 elementParams.mSupportsSplitLayout = a.getBoolean(
                         R.styleable.KeyboardLayoutSet_Element_supportsSplitLayout, false);
+                elementParams.mAllowRedundantMoreKeys = a.getBoolean(
+                        R.styleable.KeyboardLayoutSet_Element_allowRedundantMoreKeys, true);
                 mParams.mKeyboardLayoutSetElementIdToParamsMap.put(elementName, elementParams);
             } finally {
                 a.recycle();
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java
index 2056a0b..5038555 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java
@@ -162,6 +162,10 @@
         mParams.mKeysCache = keysCache;
     }
 
+    public void setAllowRedundantMoreKes(final boolean enabled) {
+        mParams.mAllowRedundantMoreKeys = enabled;
+    }
+
     public KeyboardBuilder<KP> load(final int xmlId, final KeyboardId id) {
         mParams.mId = id;
         final XmlResourceParser parser = mResources.getXml(xmlId);
@@ -851,6 +855,7 @@
     }
 
     private void endKeyboard() {
+        mParams.removeRedundantMoreKeys();
         // {@link #parseGridRows(XmlPullParser,boolean)} may populate keyboard rows higher than
         // previously expected.
         final int actualHeight = mCurrentY - mParams.mVerticalGap + mParams.mBottomPadding;
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardParams.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardParams.java
index 5df9d3e..1e1188b 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardParams.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardParams.java
@@ -68,6 +68,7 @@
     public final KeyStylesSet mKeyStyles = new KeyStylesSet(mTextsSet);
 
     public KeysCache mKeysCache;
+    public boolean mAllowRedundantMoreKeys;
 
     public int mMostCommonKeyHeight = 0;
     public int mMostCommonKeyWidth = 0;
@@ -115,6 +116,23 @@
         }
     }
 
+    public void removeRedundantMoreKeys() {
+        if (mAllowRedundantMoreKeys) {
+            return;
+        }
+        final MoreKeySpec.LettersOnBaseLayout lettersOnBaseLayout =
+                new MoreKeySpec.LettersOnBaseLayout();
+        for (final Key key : mSortedKeys) {
+            lettersOnBaseLayout.addLetter(key);
+        }
+        final ArrayList<Key> allKeys = new ArrayList<>(mSortedKeys);
+        mSortedKeys.clear();
+        for (final Key key : allKeys) {
+            final Key filteredKey = Key.removeRedundantMoreKeys(key, lettersOnBaseLayout);
+            mSortedKeys.add(mKeysCache.replace(key, filteredKey));
+        }
+    }
+
     private int mMaxHeightCount = 0;
     private int mMaxWidthCount = 0;
     private final SparseIntArray mHeightHistogram = new SparseIntArray();
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeysCache.java b/java/src/com/android/inputmethod/keyboard/internal/KeysCache.java
index 7743d47..e867863 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeysCache.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeysCache.java
@@ -36,4 +36,12 @@
         mMap.put(key, key);
         return key;
     }
+
+    public Key replace(final Key oldKey, final Key newKey) {
+        if (oldKey.equals(newKey)) {
+            return oldKey;
+        }
+        mMap.remove(oldKey);
+        return get(newKey);
+    }
 }
diff --git a/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java b/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java
index 625a0c2..764159c 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java
@@ -17,7 +17,9 @@
 package com.android.inputmethod.keyboard.internal;
 
 import android.text.TextUtils;
+import android.util.SparseIntArray;
 
+import com.android.inputmethod.compat.CharacterCompat;
 import com.android.inputmethod.keyboard.Key;
 import com.android.inputmethod.latin.Constants;
 import com.android.inputmethod.latin.define.DebugFlags;
@@ -26,6 +28,7 @@
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashSet;
 import java.util.Locale;
 
 /**
@@ -110,6 +113,46 @@
         }
     }
 
+    public static class LettersOnBaseLayout {
+        private final SparseIntArray mCodes = new SparseIntArray();
+        private final HashSet<String> mTexts = new HashSet<>();
+
+        public void addLetter(final Key key) {
+            final int code = key.getCode();
+            if (CharacterCompat.isAlphabetic(code)) {
+                mCodes.put(code, 0);
+            } else if (code == Constants.CODE_OUTPUT_TEXT) {
+                mTexts.add(key.getOutputText());
+            }
+        }
+
+        public boolean contains(final MoreKeySpec moreKey) {
+            final int code = moreKey.mCode;
+            if (CharacterCompat.isAlphabetic(code) && mCodes.indexOfKey(code) >= 0) {
+                return true;
+            } else if (code == Constants.CODE_OUTPUT_TEXT && mTexts.contains(moreKey.mOutputText)) {
+                return true;
+            }
+            return false;
+        }
+    }
+
+    public static MoreKeySpec[] removeRedundantMoreKeys(final MoreKeySpec[] moreKeys,
+            final LettersOnBaseLayout lettersOnBaseLayout) {
+        if (moreKeys == null) {
+            return null;
+        }
+        final ArrayList<MoreKeySpec> filteredMoreKeys = new ArrayList<>();
+        for (final MoreKeySpec moreKey : moreKeys) {
+            if (!lettersOnBaseLayout.contains(moreKey)) {
+                filteredMoreKeys.add(moreKey);
+            }
+        }
+        final int size = filteredMoreKeys.size();
+        return (moreKeys.length == size) ? moreKeys
+                : filteredMoreKeys.toArray(new MoreKeySpec[size]);
+    }
+
     private static final boolean DEBUG = DebugFlags.DEBUG_ENABLED;
     // Constants for parsing.
     private static final char COMMA = Constants.CODE_COMMA;