diff --git a/java/res/values-land/dimens.xml b/java/res/values-land/dimens.xml
index c78c25f8620a858d045fae751e4075bd348749bc..d411cb954b2b4407814659698c388f38f857918a 100644
--- a/java/res/values-land/dimens.xml
+++ b/java/res/values-land/dimens.xml
@@ -79,4 +79,7 @@
     <dimen name="gesture_floating_preview_text_offset">54dp</dimen>
     <dimen name="gesture_floating_preview_horizontal_padding">23dp</dimen>
     <dimen name="gesture_floating_preview_vertical_padding">15dp</dimen>
+
+    <!-- Emoji keyboard -->
+    <fraction name="emoji_keyboard_key_width">8.3333%p</fraction>
 </resources>
diff --git a/java/res/values/attrs.xml b/java/res/values/attrs.xml
index 4c5097654c9a9fe8440c4f58c12aaaca9bd33709..7ebaf75bebf21a2f8e054b6d21fe81dd044d802a 100644
--- a/java/res/values/attrs.xml
+++ b/java/res/values/attrs.xml
@@ -215,6 +215,11 @@
         <attr name="iconEmojiKey" format="reference" />
     </declare-styleable>
 
+    <declare-styleable name="Keyboard_GridRows">
+        <attr name="codesArray" format="reference" />
+        <attr name="textsArray" format="reference" />
+    </declare-styleable>
+
     <declare-styleable name="Keyboard_Key">
         <!-- The unicode value that this key outputs.
              Code value represented in hexadecimal prefixed with "0x" or code value reference using
diff --git a/java/res/values/dimens.xml b/java/res/values/dimens.xml
index 98ae76cb153cf03233ee883f09a39066df3b81a0..bb5f0bb8be100006726949821bb1d112ca2d7c36 100644
--- a/java/res/values/dimens.xml
+++ b/java/res/values/dimens.xml
@@ -123,6 +123,9 @@
     <dimen name="gesture_floating_preview_vertical_padding">16dp</dimen>
     <dimen name="gesture_floating_preview_round_radius">3dp</dimen>
 
+    <!-- Emoji keyboard -->
+    <fraction name="emoji_keyboard_key_width">14.2857%p</fraction>
+
     <!-- Inset used in Accessibility mode to avoid accidental key presses when a finger slides off the screen. -->
     <dimen name="accessibility_edge_slop">8dp</dimen>
 
diff --git a/java/res/xml/kbd_emoji_category1.xml b/java/res/xml/kbd_emoji_category1.xml
new file mode 100644
index 0000000000000000000000000000000000000000..92b0c3fbf3689e9996a7604a59cf26d88bd2fc22
--- /dev/null
+++ b/java/res/xml/kbd_emoji_category1.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 2013, 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.
+*/
+-->
+
+<Keyboard
+    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
+    latin:keyWidth="@fraction/emoji_keyboard_key_width"
+    latin:keyLetterSize="90%p"
+>
+    <GridRows
+        latin:codesArray="@array/emoji_faces"
+        latin:keyLabelFlags="fontNormal"
+        latin:backgroundType="empty" />
+</Keyboard>
diff --git a/java/res/xml/kbd_emoji_category2.xml b/java/res/xml/kbd_emoji_category2.xml
new file mode 100644
index 0000000000000000000000000000000000000000..17d36c52f2bcd413c4cc5a8ffc845b2c0f40d301
--- /dev/null
+++ b/java/res/xml/kbd_emoji_category2.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 2013, 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.
+*/
+-->
+
+<Keyboard
+    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
+    latin:keyWidth="@fraction/emoji_keyboard_key_width"
+    latin:keyLetterSize="90%p"
+>
+    <GridRows
+        latin:codesArray="@array/emoji_objects"
+        latin:keyLabelFlags="fontNormal"
+        latin:backgroundType="empty" />
+</Keyboard>
diff --git a/java/res/xml/kbd_emoji_category3.xml b/java/res/xml/kbd_emoji_category3.xml
new file mode 100644
index 0000000000000000000000000000000000000000..9000a3a11df921d26eff8c92ebd080c86fed1f72
--- /dev/null
+++ b/java/res/xml/kbd_emoji_category3.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 2013, 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.
+*/
+-->
+
+<Keyboard
+    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
+    latin:keyWidth="@fraction/emoji_keyboard_key_width"
+    latin:keyLetterSize="90%p"
+>
+    <GridRows
+        latin:codesArray="@array/emoji_nature"
+        latin:keyLabelFlags="fontNormal"
+        latin:backgroundType="empty" />
+</Keyboard>
diff --git a/java/res/xml/kbd_emoji_category4.xml b/java/res/xml/kbd_emoji_category4.xml
new file mode 100644
index 0000000000000000000000000000000000000000..e79e124e758b2d46b610f930728d3ff9a558c057
--- /dev/null
+++ b/java/res/xml/kbd_emoji_category4.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 2013, 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.
+*/
+-->
+
+<Keyboard
+    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
+    latin:keyWidth="@fraction/emoji_keyboard_key_width"
+    latin:keyLetterSize="90%p"
+>
+    <GridRows
+        latin:codesArray="@array/emoji_places"
+        latin:keyLabelFlags="fontNormal"
+        latin:backgroundType="empty" />
+</Keyboard>
diff --git a/java/res/xml/kbd_emoji_category5.xml b/java/res/xml/kbd_emoji_category5.xml
new file mode 100644
index 0000000000000000000000000000000000000000..07b3d908cea1504a3442cb46c4ca47b07d4ea04e
--- /dev/null
+++ b/java/res/xml/kbd_emoji_category5.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 2013, 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.
+*/
+-->
+
+<Keyboard
+    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
+    latin:keyWidth="@fraction/emoji_keyboard_key_width"
+    latin:keyLetterSize="90%p"
+>
+    <GridRows
+        latin:codesArray="@array/emoji_symbols"
+        latin:keyLabelFlags="fontNormal"
+        latin:backgroundType="empty" />
+</Keyboard>
diff --git a/java/res/xml/kbd_emoji_category6.xml b/java/res/xml/kbd_emoji_category6.xml
new file mode 100644
index 0000000000000000000000000000000000000000..a07966b07ca5516c51cde625dc09834a87810e4c
--- /dev/null
+++ b/java/res/xml/kbd_emoji_category6.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 2013, 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.
+*/
+-->
+
+<Keyboard
+    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
+    latin:keyWidth="@fraction/emoji_keyboard_key_width"
+    latin:keyLetterSize="90%p"
+>
+    <GridRows
+        latin:textsArray="@array/emoji_emoticons"
+        latin:keyLabelFlags="fontNormal"
+        latin:backgroundType="empty" />
+</Keyboard>
diff --git a/java/res/xml/kbd_emoji_recents.xml b/java/res/xml/kbd_emoji_recents.xml
new file mode 100644
index 0000000000000000000000000000000000000000..8b4fa958c2c2c9f805a236970ec5ddbe113c5dae
--- /dev/null
+++ b/java/res/xml/kbd_emoji_recents.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 2013, 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.
+*/
+-->
+
+<Keyboard
+    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
+    latin:keyWidth="@fraction/emoji_keyboard_key_width"
+    latin:keyLetterSize="90%p"
+>
+    <GridRows
+        latin:codesArray="@array/emoji_recents"
+        latin:keyLabelFlags="fontNormal"
+        latin:backgroundType="empty" />
+</Keyboard>
diff --git a/java/res/xml/keyboard_layout_set_emoji.xml b/java/res/xml/keyboard_layout_set_emoji.xml
new file mode 100644
index 0000000000000000000000000000000000000000..98e6b6b5c8ef7776e508d522f8585d90c6b1b3c7
--- /dev/null
+++ b/java/res/xml/keyboard_layout_set_emoji.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 2013, 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.
+*/
+-->
+
+<KeyboardLayoutSet
+    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin">
+    <Element
+        latin:elementName="emojiRecents"
+        latin:elementKeyboard="@xml/kbd_emoji_recents" />
+    <Element
+        latin:elementName="emojiCategory1"
+        latin:elementKeyboard="@xml/kbd_emoji_category1" />
+    <Element
+        latin:elementName="emojiCategory2"
+        latin:elementKeyboard="@xml/kbd_emoji_category2" />
+    <Element
+        latin:elementName="emojiCategory3"
+        latin:elementKeyboard="@xml/kbd_emoji_category3" />
+    <Element
+        latin:elementName="emojiCategory4"
+        latin:elementKeyboard="@xml/kbd_emoji_category4" />
+    <Element
+        latin:elementName="emojiCategory5"
+        latin:elementKeyboard="@xml/kbd_emoji_category5" />
+    <Element
+        latin:elementName="emojiCategory6"
+        latin:elementKeyboard="@xml/kbd_emoji_category6" />
+</KeyboardLayoutSet>
diff --git a/java/src/com/android/inputmethod/keyboard/Keyboard.java b/java/src/com/android/inputmethod/keyboard/Keyboard.java
index 0b3737e485f3b57b1984a4d124f0237c4bbfb800..23f037fbdb749de7a535fb123d8fcfd4d3aa2605 100644
--- a/java/src/com/android/inputmethod/keyboard/Keyboard.java
+++ b/java/src/com/android/inputmethod/keyboard/Keyboard.java
@@ -51,6 +51,11 @@ public class Keyboard {
     /** Total width of the keyboard, including the padding and keys */
     public final int mOccupiedWidth;
 
+    /** Base height of the keyboard, used to calculate rows' height */
+    public final int mBaseHeight;
+    /** Base width of the keyboard, used to calculate keys' width */
+    public final int mBaseWidth;
+
     /** The padding above the keyboard */
     public final int mTopPadding;
     /** Default gap between rows */
@@ -84,6 +89,8 @@ public class Keyboard {
         mThemeId = params.mThemeId;
         mOccupiedHeight = params.mOccupiedHeight;
         mOccupiedWidth = params.mOccupiedWidth;
+        mBaseHeight = params.mBaseHeight;
+        mBaseWidth = params.mBaseWidth;
         mMostCommonKeyHeight = params.mMostCommonKeyHeight;
         mMostCommonKeyWidth = params.mMostCommonKeyWidth;
         mMoreKeysTemplate = params.mMoreKeysTemplate;
@@ -109,6 +116,8 @@ public class Keyboard {
         mThemeId = keyboard.mThemeId;
         mOccupiedHeight = keyboard.mOccupiedHeight;
         mOccupiedWidth = keyboard.mOccupiedWidth;
+        mBaseHeight = keyboard.mBaseHeight;
+        mBaseWidth = keyboard.mBaseWidth;
         mMostCommonKeyHeight = keyboard.mMostCommonKeyHeight;
         mMostCommonKeyWidth = keyboard.mMostCommonKeyWidth;
         mMoreKeysTemplate = keyboard.mMoreKeysTemplate;
diff --git a/java/src/com/android/inputmethod/keyboard/internal/CodesArrayParser.java b/java/src/com/android/inputmethod/keyboard/internal/CodesArrayParser.java
new file mode 100644
index 0000000000000000000000000000000000000000..c10fdbaceafcb4a71e99907784ca84a3c4af65ee
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/CodesArrayParser.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2013 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.keyboard.internal;
+
+import com.android.inputmethod.latin.Constants;
+
+/**
+ * The string parser of codesArray specification for <GridRows />. The attribute codesArray is an
+ * array of string.
+ * Each element of the array defines a key label by specifying a code point as a hexadecimal string.
+ * A key label may consist of multiple code points separated by comma.
+ * Each element of the array optionally can have an output text definition after vertical bar
+ * marker. An output text may consist of multiple code points separated by comma.
+ * The format of the codesArray element should be:
+ * <pre>
+ *   codePointInHex[,codePoint2InHex]*(|outputTextCodePointInHex[,outputTextCodePoint2InHex]*)?
+ * </pre>
+ */
+// TODO: Write unit tests for this class.
+public final class CodesArrayParser {
+    // Constants for parsing.
+    private static final char COMMA = ',';
+    private static final char VERTICAL_BAR = '|';
+    private static final String COMMA_STRING = ",";
+    private static final int BASE_HEX = 16;
+
+    private CodesArrayParser() {
+     // This utility class is not publicly instantiable.
+    }
+
+    private static String getLabelSpec(final String codesArraySpec) {
+        final int pos = codesArraySpec.indexOf(VERTICAL_BAR);
+        return (pos < 0) ? codesArraySpec : codesArraySpec.substring(0, pos);
+    }
+
+    public static String parseLabel(final String codesArraySpec) {
+        final String labelSpec = getLabelSpec(codesArraySpec);
+        final StringBuilder sb = new StringBuilder();
+        for (final String codeInHex : labelSpec.split(COMMA_STRING)) {
+            final int codePoint = Integer.parseInt(codeInHex, BASE_HEX);
+            sb.appendCodePoint(codePoint);
+        }
+        return sb.toString();
+    }
+
+    private static String getCodeSpec(final String codesArraySpec) {
+        final int pos = codesArraySpec.indexOf(VERTICAL_BAR);
+        return (pos < 0) ? codesArraySpec : codesArraySpec.substring(pos + 1);
+    }
+
+    public static int parseCode(final String codesArraySpec) {
+        final String codeSpec = getCodeSpec(codesArraySpec);
+        if (codeSpec.indexOf(COMMA) < 0) {
+            return Integer.parseInt(codeSpec, BASE_HEX);
+        }
+        return Constants.CODE_OUTPUT_TEXT;
+    }
+
+    public static String parseOutputText(final String codesArraySpec) {
+        final String codeSpec = getCodeSpec(codesArraySpec);
+        if (codeSpec.indexOf(COMMA) < 0) {
+            return null;
+        }
+        final StringBuilder sb = new StringBuilder();
+        for (final String codeInHex : codeSpec.split(COMMA_STRING)) {
+            final int codePoint = Integer.parseInt(codeInHex, BASE_HEX);
+            sb.appendCodePoint(codePoint);
+        }
+        return sb.toString();
+    }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java
index 3f0773e156a18ccd60eed8919b5023afbab3f307..8c70389ba9c43f76ce1c626100c6509e97d40309 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java
@@ -29,6 +29,7 @@ import com.android.inputmethod.annotations.UsedForTesting;
 import com.android.inputmethod.keyboard.Key;
 import com.android.inputmethod.keyboard.Keyboard;
 import com.android.inputmethod.keyboard.KeyboardId;
+import com.android.inputmethod.latin.Constants;
 import com.android.inputmethod.latin.R;
 import com.android.inputmethod.latin.utils.ResourceUtils;
 import com.android.inputmethod.latin.utils.RunInLocale;
@@ -113,6 +114,7 @@ import java.util.Locale;
  * </pre>
  */
 
+// TODO: Write unit tests for this class.
 public class KeyboardBuilder<KP extends KeyboardParams> {
     private static final String BUILDER_TAG = "Keyboard.Builder";
     private static final boolean DEBUG = false;
@@ -120,6 +122,7 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
     // Keyboard XML Tags
     private static final String TAG_KEYBOARD = "Keyboard";
     private static final String TAG_ROW = "Row";
+    private static final String TAG_GRID_ROWS = "GridRows";
     private static final String TAG_KEY = "Key";
     private static final String TAG_SPACER = "Spacer";
     private static final String TAG_INCLUDE = "include";
@@ -312,6 +315,9 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
                         startRow(row);
                     }
                     parseRowContent(parser, row, skip);
+                } else if (TAG_GRID_ROWS.equals(tag)) {
+                    if (DEBUG) startTag("<%s>%s", TAG_GRID_ROWS, skip ? " skipped" : "");
+                    parseGridRows(parser, skip);
                 } else if (TAG_INCLUDE.equals(tag)) {
                     parseIncludeKeyboardContent(parser, skip);
                 } else if (TAG_SWITCH.equals(tag)) {
@@ -389,6 +395,73 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
         }
     }
 
+    private void parseGridRows(final XmlPullParser parser, final boolean skip)
+            throws XmlPullParserException, IOException {
+        if (skip) {
+            XmlParseUtils.checkEndTag(TAG_GRID_ROWS, parser);
+            if (DEBUG) {
+                startEndTag("<%s /> skipped", TAG_GRID_ROWS);
+            }
+            return;
+        }
+        final KeyboardRow gridRows = new KeyboardRow(mResources, mParams, parser, mCurrentY);
+        final TypedArray gridRowAttr = mResources.obtainAttributes(
+                Xml.asAttributeSet(parser), R.styleable.Keyboard_GridRows);
+        final int codesArrayId = gridRowAttr.getResourceId(
+                R.styleable.Keyboard_GridRows_codesArray, 0);
+        final int textsArrayId = gridRowAttr.getResourceId(
+                R.styleable.Keyboard_GridRows_textsArray, 0);
+        gridRowAttr.recycle();
+        if (codesArrayId == 0 && textsArrayId == 0) {
+            throw new XmlParseUtils.ParseException(
+                    "Missing codesArray or textsArray attributes", parser);
+        }
+        if (codesArrayId != 0 && textsArrayId != 0) {
+            throw new XmlParseUtils.ParseException(
+                    "Both codesArray and textsArray attributes specifed", parser);
+        }
+        final String[] array = mResources.getStringArray(
+                codesArrayId != 0 ? codesArrayId : textsArrayId);
+        final int counts = array.length;
+        final float keyWidth = gridRows.getKeyWidth(null, 0.0f);
+        final int numColumns = (int)(mParams.mOccupiedWidth / keyWidth);
+        for (int index = 0; index < counts; index += numColumns) {
+            final KeyboardRow row = new KeyboardRow(mResources, mParams, parser, mCurrentY);
+            startRow(row);
+            for (int c = 0; c < numColumns; c++) {
+                final int i = index + c;
+                if (i >= counts) {
+                    break;
+                }
+                final String label;
+                final int code;
+                final String outputText;
+                if (codesArrayId != 0) {
+                    final String codeArraySpec = array[i];
+                    label = CodesArrayParser.parseLabel(codeArraySpec);
+                    code = CodesArrayParser.parseCode(codeArraySpec);
+                    outputText = CodesArrayParser.parseOutputText(codeArraySpec);
+                } else {
+                    final String textArraySpec = array[i];
+                    // TODO: Utilize KeySpecParser or write more generic TextsArrayParser.
+                    label = textArraySpec;
+                    code = Constants.CODE_OUTPUT_TEXT;
+                    outputText = textArraySpec + (char)Constants.CODE_SPACE;
+                }
+                final int x = (int)row.getKeyX(null);
+                final int y = row.getKeyY();
+                final Key key = new Key(mParams, label, null /* hintLabel */, 0 /* iconId */,
+                        code, outputText, x, y, (int)keyWidth, (int)row.getRowHeight(),
+                        row.getDefaultKeyLabelFlags(), row.getDefaultBackgroundType());
+                endKey(key);
+                row.advanceXPos(keyWidth);
+            }
+            endRow(row);
+        }
+
+        XmlParseUtils.checkEndTag(TAG_GRID_ROWS, parser);
+    }
+
     private void parseKey(final XmlPullParser parser, final KeyboardRow row, final boolean skip)
             throws XmlPullParserException, IOException {
         if (skip) {
@@ -744,7 +817,10 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
     }
 
     private void endKeyboard() {
-        // nothing to do here.
+        // {@link #parseGridRows(XmlPullParser,boolean)} may populate keyboard rows higher than
+        // previously expected.
+        final int actualHeight = mCurrentY - mParams.mVerticalGap + mParams.mBottomPadding;
+        mParams.mOccupiedHeight = Math.max(mParams.mOccupiedHeight, actualHeight);
     }
 
     private void addEdgeSpace(final float width, final KeyboardRow row) {