Home | History | Annotate | Download | only in internal
      1 /*
      2  * Copyright (C) 2012 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.inputmethod.keyboard.internal;
     18 
     19 import android.content.Context;
     20 import android.content.res.Resources;
     21 import android.content.res.TypedArray;
     22 import android.content.res.XmlResourceParser;
     23 import android.os.Build;
     24 import android.text.TextUtils;
     25 import android.util.AttributeSet;
     26 import android.util.Log;
     27 import android.util.TypedValue;
     28 import android.util.Xml;
     29 
     30 import com.android.inputmethod.annotations.UsedForTesting;
     31 import com.android.inputmethod.keyboard.Key;
     32 import com.android.inputmethod.keyboard.Keyboard;
     33 import com.android.inputmethod.keyboard.KeyboardId;
     34 import com.android.inputmethod.keyboard.KeyboardTheme;
     35 import com.android.inputmethod.latin.R;
     36 import com.android.inputmethod.latin.common.Constants;
     37 import com.android.inputmethod.latin.common.StringUtils;
     38 import com.android.inputmethod.latin.utils.ResourceUtils;
     39 import com.android.inputmethod.latin.utils.XmlParseUtils;
     40 import com.android.inputmethod.latin.utils.XmlParseUtils.ParseException;
     41 
     42 import org.xmlpull.v1.XmlPullParser;
     43 import org.xmlpull.v1.XmlPullParserException;
     44 
     45 import java.io.IOException;
     46 import java.util.Arrays;
     47 import java.util.Locale;
     48 
     49 import javax.annotation.Nonnull;
     50 
     51 /**
     52  * Keyboard Building helper.
     53  *
     54  * This class parses Keyboard XML file and eventually build a Keyboard.
     55  * The Keyboard XML file looks like:
     56  * <pre>
     57  *   &lt;!-- xml/keyboard.xml --&gt;
     58  *   &lt;Keyboard keyboard_attributes*&gt;
     59  *     &lt;!-- Keyboard Content --&gt;
     60  *     &lt;Row row_attributes*&gt;
     61  *       &lt;!-- Row Content --&gt;
     62  *       &lt;Key key_attributes* /&gt;
     63  *       &lt;Spacer horizontalGap="32.0dp" /&gt;
     64  *       &lt;include keyboardLayout="@xml/other_keys"&gt;
     65  *       ...
     66  *     &lt;/Row&gt;
     67  *     &lt;include keyboardLayout="@xml/other_rows"&gt;
     68  *     ...
     69  *   &lt;/Keyboard&gt;
     70  * </pre>
     71  * The XML file which is included in other file must have &lt;merge&gt; as root element,
     72  * such as:
     73  * <pre>
     74  *   &lt;!-- xml/other_keys.xml --&gt;
     75  *   &lt;merge&gt;
     76  *     &lt;Key key_attributes* /&gt;
     77  *     ...
     78  *   &lt;/merge&gt;
     79  * </pre>
     80  * and
     81  * <pre>
     82  *   &lt;!-- xml/other_rows.xml --&gt;
     83  *   &lt;merge&gt;
     84  *     &lt;Row row_attributes*&gt;
     85  *       &lt;Key key_attributes* /&gt;
     86  *     &lt;/Row&gt;
     87  *     ...
     88  *   &lt;/merge&gt;
     89  * </pre>
     90  * You can also use switch-case-default tags to select Rows and Keys.
     91  * <pre>
     92  *   &lt;switch&gt;
     93  *     &lt;case case_attribute*&gt;
     94  *       &lt;!-- Any valid tags at switch position --&gt;
     95  *     &lt;/case&gt;
     96  *     ...
     97  *     &lt;default&gt;
     98  *       &lt;!-- Any valid tags at switch position --&gt;
     99  *     &lt;/default&gt;
    100  *   &lt;/switch&gt;
    101  * </pre>
    102  * You can declare Key style and specify styles within Key tags.
    103  * <pre>
    104  *     &lt;switch&gt;
    105  *       &lt;case mode="email"&gt;
    106  *         &lt;key-style styleName="f1-key" parentStyle="modifier-key"
    107  *           keyLabel=".com"
    108  *         /&gt;
    109  *       &lt;/case&gt;
    110  *       &lt;case mode="url"&gt;
    111  *         &lt;key-style styleName="f1-key" parentStyle="modifier-key"
    112  *           keyLabel="http://"
    113  *         /&gt;
    114  *       &lt;/case&gt;
    115  *     &lt;/switch&gt;
    116  *     ...
    117  *     &lt;Key keyStyle="shift-key" ... /&gt;
    118  * </pre>
    119  */
    120 
    121 // TODO: Write unit tests for this class.
    122 public class KeyboardBuilder<KP extends KeyboardParams> {
    123     private static final String BUILDER_TAG = "Keyboard.Builder";
    124     private static final boolean DEBUG = false;
    125 
    126     // Keyboard XML Tags
    127     private static final String TAG_KEYBOARD = "Keyboard";
    128     private static final String TAG_ROW = "Row";
    129     private static final String TAG_GRID_ROWS = "GridRows";
    130     private static final String TAG_KEY = "Key";
    131     private static final String TAG_SPACER = "Spacer";
    132     private static final String TAG_INCLUDE = "include";
    133     private static final String TAG_MERGE = "merge";
    134     private static final String TAG_SWITCH = "switch";
    135     private static final String TAG_CASE = "case";
    136     private static final String TAG_DEFAULT = "default";
    137     public static final String TAG_KEY_STYLE = "key-style";
    138 
    139     private static final int DEFAULT_KEYBOARD_COLUMNS = 10;
    140     private static final int DEFAULT_KEYBOARD_ROWS = 4;
    141 
    142     @Nonnull
    143     protected final KP mParams;
    144     protected final Context mContext;
    145     protected final Resources mResources;
    146 
    147     private int mCurrentY = 0;
    148     private KeyboardRow mCurrentRow = null;
    149     private boolean mLeftEdge;
    150     private boolean mTopEdge;
    151     private Key mRightEdgeKey = null;
    152 
    153     public KeyboardBuilder(final Context context, @Nonnull final KP params) {
    154         mContext = context;
    155         final Resources res = context.getResources();
    156         mResources = res;
    157 
    158         mParams = params;
    159 
    160         params.GRID_WIDTH = res.getInteger(R.integer.config_keyboard_grid_width);
    161         params.GRID_HEIGHT = res.getInteger(R.integer.config_keyboard_grid_height);
    162     }
    163 
    164     public void setAllowRedundantMoreKes(final boolean enabled) {
    165         mParams.mAllowRedundantMoreKeys = enabled;
    166     }
    167 
    168     public KeyboardBuilder<KP> load(final int xmlId, final KeyboardId id) {
    169         mParams.mId = id;
    170         final XmlResourceParser parser = mResources.getXml(xmlId);
    171         try {
    172             parseKeyboard(parser);
    173         } catch (XmlPullParserException e) {
    174             Log.w(BUILDER_TAG, "keyboard XML parse error", e);
    175             throw new IllegalArgumentException(e.getMessage(), e);
    176         } catch (IOException e) {
    177             Log.w(BUILDER_TAG, "keyboard XML parse error", e);
    178             throw new RuntimeException(e.getMessage(), e);
    179         } finally {
    180             parser.close();
    181         }
    182         return this;
    183     }
    184 
    185     @UsedForTesting
    186     public void disableTouchPositionCorrectionDataForTest() {
    187         mParams.mTouchPositionCorrection.setEnabled(false);
    188     }
    189 
    190     public void setProximityCharsCorrectionEnabled(final boolean enabled) {
    191         mParams.mProximityCharsCorrectionEnabled = enabled;
    192     }
    193 
    194     @Nonnull
    195     public Keyboard build() {
    196         return new Keyboard(mParams);
    197     }
    198 
    199     private int mIndent;
    200     private static final String SPACES = "                                             ";
    201 
    202     private static String spaces(final int count) {
    203         return (count < SPACES.length()) ? SPACES.substring(0, count) : SPACES;
    204     }
    205 
    206     private void startTag(final String format, final Object ... args) {
    207         Log.d(BUILDER_TAG, String.format(spaces(++mIndent * 2) + format, args));
    208     }
    209 
    210     private void endTag(final String format, final Object ... args) {
    211         Log.d(BUILDER_TAG, String.format(spaces(mIndent-- * 2) + format, args));
    212     }
    213 
    214     private void startEndTag(final String format, final Object ... args) {
    215         Log.d(BUILDER_TAG, String.format(spaces(++mIndent * 2) + format, args));
    216         mIndent--;
    217     }
    218 
    219     private void parseKeyboard(final XmlPullParser parser)
    220             throws XmlPullParserException, IOException {
    221         if (DEBUG) startTag("<%s> %s", TAG_KEYBOARD, mParams.mId);
    222         while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
    223             final int event = parser.next();
    224             if (event == XmlPullParser.START_TAG) {
    225                 final String tag = parser.getName();
    226                 if (TAG_KEYBOARD.equals(tag)) {
    227                     parseKeyboardAttributes(parser);
    228                     startKeyboard();
    229                     parseKeyboardContent(parser, false);
    230                     return;
    231                 }
    232                 throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD);
    233             }
    234         }
    235     }
    236 
    237     private void parseKeyboardAttributes(final XmlPullParser parser) {
    238         final AttributeSet attr = Xml.asAttributeSet(parser);
    239         final TypedArray keyboardAttr = mContext.obtainStyledAttributes(
    240                 attr, R.styleable.Keyboard, R.attr.keyboardStyle, R.style.Keyboard);
    241         final TypedArray keyAttr = mResources.obtainAttributes(attr, R.styleable.Keyboard_Key);
    242         try {
    243             final KeyboardParams params = mParams;
    244             final int height = params.mId.mHeight;
    245             final int width = params.mId.mWidth;
    246             params.mOccupiedHeight = height;
    247             params.mOccupiedWidth = width;
    248             params.mTopPadding = (int)keyboardAttr.getFraction(
    249                     R.styleable.Keyboard_keyboardTopPadding, height, height, 0);
    250             params.mBottomPadding = (int)keyboardAttr.getFraction(
    251                     R.styleable.Keyboard_keyboardBottomPadding, height, height, 0);
    252             params.mLeftPadding = (int)keyboardAttr.getFraction(
    253                     R.styleable.Keyboard_keyboardLeftPadding, width, width, 0);
    254             params.mRightPadding = (int)keyboardAttr.getFraction(
    255                     R.styleable.Keyboard_keyboardRightPadding, width, width, 0);
    256 
    257             final int baseWidth =
    258                     params.mOccupiedWidth - params.mLeftPadding - params.mRightPadding;
    259             params.mBaseWidth = baseWidth;
    260             params.mDefaultKeyWidth = (int)keyAttr.getFraction(R.styleable.Keyboard_Key_keyWidth,
    261                     baseWidth, baseWidth, baseWidth / DEFAULT_KEYBOARD_COLUMNS);
    262             params.mHorizontalGap = (int)keyboardAttr.getFraction(
    263                     R.styleable.Keyboard_horizontalGap, baseWidth, baseWidth, 0);
    264             // TODO: Fix keyboard geometry calculation clearer. Historically vertical gap between
    265             // rows are determined based on the entire keyboard height including top and bottom
    266             // paddings.
    267             params.mVerticalGap = (int)keyboardAttr.getFraction(
    268                     R.styleable.Keyboard_verticalGap, height, height, 0);
    269             final int baseHeight = params.mOccupiedHeight - params.mTopPadding
    270                     - params.mBottomPadding + params.mVerticalGap;
    271             params.mBaseHeight = baseHeight;
    272             params.mDefaultRowHeight = (int)ResourceUtils.getDimensionOrFraction(keyboardAttr,
    273                     R.styleable.Keyboard_rowHeight, baseHeight, baseHeight / DEFAULT_KEYBOARD_ROWS);
    274 
    275             params.mKeyVisualAttributes = KeyVisualAttributes.newInstance(keyAttr);
    276 
    277             params.mMoreKeysTemplate = keyboardAttr.getResourceId(
    278                     R.styleable.Keyboard_moreKeysTemplate, 0);
    279             params.mMaxMoreKeysKeyboardColumn = keyAttr.getInt(
    280                     R.styleable.Keyboard_Key_maxMoreKeysColumn, 5);
    281 
    282             params.mThemeId = keyboardAttr.getInt(R.styleable.Keyboard_themeId, 0);
    283             params.mIconsSet.loadIcons(keyboardAttr);
    284             params.mTextsSet.setLocale(params.mId.getLocale(), mContext);
    285 
    286             final int resourceId = keyboardAttr.getResourceId(
    287                     R.styleable.Keyboard_touchPositionCorrectionData, 0);
    288             if (resourceId != 0) {
    289                 final String[] data = mResources.getStringArray(resourceId);
    290                 params.mTouchPositionCorrection.load(data);
    291             }
    292         } finally {
    293             keyAttr.recycle();
    294             keyboardAttr.recycle();
    295         }
    296     }
    297 
    298     private void parseKeyboardContent(final XmlPullParser parser, final boolean skip)
    299             throws XmlPullParserException, IOException {
    300         while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
    301             final int event = parser.next();
    302             if (event == XmlPullParser.START_TAG) {
    303                 final String tag = parser.getName();
    304                 if (TAG_ROW.equals(tag)) {
    305                     final KeyboardRow row = parseRowAttributes(parser);
    306                     if (DEBUG) startTag("<%s>%s", TAG_ROW, skip ? " skipped" : "");
    307                     if (!skip) {
    308                         startRow(row);
    309                     }
    310                     parseRowContent(parser, row, skip);
    311                 } else if (TAG_GRID_ROWS.equals(tag)) {
    312                     if (DEBUG) startTag("<%s>%s", TAG_GRID_ROWS, skip ? " skipped" : "");
    313                     parseGridRows(parser, skip);
    314                 } else if (TAG_INCLUDE.equals(tag)) {
    315                     parseIncludeKeyboardContent(parser, skip);
    316                 } else if (TAG_SWITCH.equals(tag)) {
    317                     parseSwitchKeyboardContent(parser, skip);
    318                 } else if (TAG_KEY_STYLE.equals(tag)) {
    319                     parseKeyStyle(parser, skip);
    320                 } else {
    321                     throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_ROW);
    322                 }
    323             } else if (event == XmlPullParser.END_TAG) {
    324                 final String tag = parser.getName();
    325                 if (DEBUG) endTag("</%s>", tag);
    326                 if (TAG_KEYBOARD.equals(tag)) {
    327                     endKeyboard();
    328                     return;
    329                 }
    330                 if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag) || TAG_MERGE.equals(tag)) {
    331                     return;
    332                 }
    333                 throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_ROW);
    334             }
    335         }
    336     }
    337 
    338     private KeyboardRow parseRowAttributes(final XmlPullParser parser)
    339             throws XmlPullParserException {
    340         final AttributeSet attr = Xml.asAttributeSet(parser);
    341         final TypedArray keyboardAttr = mResources.obtainAttributes(attr, R.styleable.Keyboard);
    342         try {
    343             if (keyboardAttr.hasValue(R.styleable.Keyboard_horizontalGap)) {
    344                 throw new XmlParseUtils.IllegalAttribute(parser, TAG_ROW, "horizontalGap");
    345             }
    346             if (keyboardAttr.hasValue(R.styleable.Keyboard_verticalGap)) {
    347                 throw new XmlParseUtils.IllegalAttribute(parser, TAG_ROW, "verticalGap");
    348             }
    349             return new KeyboardRow(mResources, mParams, parser, mCurrentY);
    350         } finally {
    351             keyboardAttr.recycle();
    352         }
    353     }
    354 
    355     private void parseRowContent(final XmlPullParser parser, final KeyboardRow row,
    356             final boolean skip) throws XmlPullParserException, IOException {
    357         while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
    358             final int event = parser.next();
    359             if (event == XmlPullParser.START_TAG) {
    360                 final String tag = parser.getName();
    361                 if (TAG_KEY.equals(tag)) {
    362                     parseKey(parser, row, skip);
    363                 } else if (TAG_SPACER.equals(tag)) {
    364                     parseSpacer(parser, row, skip);
    365                 } else if (TAG_INCLUDE.equals(tag)) {
    366                     parseIncludeRowContent(parser, row, skip);
    367                 } else if (TAG_SWITCH.equals(tag)) {
    368                     parseSwitchRowContent(parser, row, skip);
    369                 } else if (TAG_KEY_STYLE.equals(tag)) {
    370                     parseKeyStyle(parser, skip);
    371                 } else {
    372                     throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_ROW);
    373                 }
    374             } else if (event == XmlPullParser.END_TAG) {
    375                 final String tag = parser.getName();
    376                 if (DEBUG) endTag("</%s>", tag);
    377                 if (TAG_ROW.equals(tag)) {
    378                     if (!skip) {
    379                         endRow(row);
    380                     }
    381                     return;
    382                 }
    383                 if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag) || TAG_MERGE.equals(tag)) {
    384                     return;
    385                 }
    386                 throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_ROW);
    387             }
    388         }
    389     }
    390 
    391     private void parseGridRows(final XmlPullParser parser, final boolean skip)
    392             throws XmlPullParserException, IOException {
    393         if (skip) {
    394             XmlParseUtils.checkEndTag(TAG_GRID_ROWS, parser);
    395             if (DEBUG) {
    396                 startEndTag("<%s /> skipped", TAG_GRID_ROWS);
    397             }
    398             return;
    399         }
    400         final KeyboardRow gridRows = new KeyboardRow(mResources, mParams, parser, mCurrentY);
    401         final TypedArray gridRowAttr = mResources.obtainAttributes(
    402                 Xml.asAttributeSet(parser), R.styleable.Keyboard_GridRows);
    403         final int codesArrayId = gridRowAttr.getResourceId(
    404                 R.styleable.Keyboard_GridRows_codesArray, 0);
    405         final int textsArrayId = gridRowAttr.getResourceId(
    406                 R.styleable.Keyboard_GridRows_textsArray, 0);
    407         gridRowAttr.recycle();
    408         if (codesArrayId == 0 && textsArrayId == 0) {
    409             throw new XmlParseUtils.ParseException(
    410                     "Missing codesArray or textsArray attributes", parser);
    411         }
    412         if (codesArrayId != 0 && textsArrayId != 0) {
    413             throw new XmlParseUtils.ParseException(
    414                     "Both codesArray and textsArray attributes specifed", parser);
    415         }
    416         final String[] array = mResources.getStringArray(
    417                 codesArrayId != 0 ? codesArrayId : textsArrayId);
    418         final int counts = array.length;
    419         final float keyWidth = gridRows.getKeyWidth(null, 0.0f);
    420         final int numColumns = (int)(mParams.mOccupiedWidth / keyWidth);
    421         for (int index = 0; index < counts; index += numColumns) {
    422             final KeyboardRow row = new KeyboardRow(mResources, mParams, parser, mCurrentY);
    423             startRow(row);
    424             for (int c = 0; c < numColumns; c++) {
    425                 final int i = index + c;
    426                 if (i >= counts) {
    427                     break;
    428                 }
    429                 final String label;
    430                 final int code;
    431                 final String outputText;
    432                 final int supportedMinSdkVersion;
    433                 if (codesArrayId != 0) {
    434                     final String codeArraySpec = array[i];
    435                     label = CodesArrayParser.parseLabel(codeArraySpec);
    436                     code = CodesArrayParser.parseCode(codeArraySpec);
    437                     outputText = CodesArrayParser.parseOutputText(codeArraySpec);
    438                     supportedMinSdkVersion =
    439                             CodesArrayParser.getMinSupportSdkVersion(codeArraySpec);
    440                 } else {
    441                     final String textArraySpec = array[i];
    442                     // TODO: Utilize KeySpecParser or write more generic TextsArrayParser.
    443                     label = textArraySpec;
    444                     code = Constants.CODE_OUTPUT_TEXT;
    445                     outputText = textArraySpec + (char)Constants.CODE_SPACE;
    446                     supportedMinSdkVersion = 0;
    447                 }
    448                 if (Build.VERSION.SDK_INT < supportedMinSdkVersion) {
    449                     continue;
    450                 }
    451                 final int labelFlags = row.getDefaultKeyLabelFlags();
    452                 // TODO: Should be able to assign default keyActionFlags as well.
    453                 final int backgroundType = row.getDefaultBackgroundType();
    454                 final int x = (int)row.getKeyX(null);
    455                 final int y = row.getKeyY();
    456                 final int width = (int)keyWidth;
    457                 final int height = row.getRowHeight();
    458                 final Key key = new Key(label, KeyboardIconsSet.ICON_UNDEFINED, code, outputText,
    459                         null /* hintLabel */, labelFlags, backgroundType, x, y, width, height,
    460                         mParams.mHorizontalGap, mParams.mVerticalGap);
    461                 endKey(key);
    462                 row.advanceXPos(keyWidth);
    463             }
    464             endRow(row);
    465         }
    466 
    467         XmlParseUtils.checkEndTag(TAG_GRID_ROWS, parser);
    468     }
    469 
    470     private void parseKey(final XmlPullParser parser, final KeyboardRow row, final boolean skip)
    471             throws XmlPullParserException, IOException {
    472         if (skip) {
    473             XmlParseUtils.checkEndTag(TAG_KEY, parser);
    474             if (DEBUG) startEndTag("<%s /> skipped", TAG_KEY);
    475             return;
    476         }
    477         final TypedArray keyAttr = mResources.obtainAttributes(
    478                 Xml.asAttributeSet(parser), R.styleable.Keyboard_Key);
    479         final KeyStyle keyStyle = mParams.mKeyStyles.getKeyStyle(keyAttr, parser);
    480         final String keySpec = keyStyle.getString(keyAttr, R.styleable.Keyboard_Key_keySpec);
    481         if (TextUtils.isEmpty(keySpec)) {
    482             throw new ParseException("Empty keySpec", parser);
    483         }
    484         final Key key = new Key(keySpec, keyAttr, keyStyle, mParams, row);
    485         keyAttr.recycle();
    486         if (DEBUG) {
    487             startEndTag("<%s%s %s moreKeys=%s />", TAG_KEY, (key.isEnabled() ? "" : " disabled"),
    488                     key, Arrays.toString(key.getMoreKeys()));
    489         }
    490         XmlParseUtils.checkEndTag(TAG_KEY, parser);
    491         endKey(key);
    492     }
    493 
    494     private void parseSpacer(final XmlPullParser parser, final KeyboardRow row, final boolean skip)
    495             throws XmlPullParserException, IOException {
    496         if (skip) {
    497             XmlParseUtils.checkEndTag(TAG_SPACER, parser);
    498             if (DEBUG) startEndTag("<%s /> skipped", TAG_SPACER);
    499             return;
    500         }
    501         final TypedArray keyAttr = mResources.obtainAttributes(
    502                 Xml.asAttributeSet(parser), R.styleable.Keyboard_Key);
    503         final KeyStyle keyStyle = mParams.mKeyStyles.getKeyStyle(keyAttr, parser);
    504         final Key spacer = new Key.Spacer(keyAttr, keyStyle, mParams, row);
    505         keyAttr.recycle();
    506         if (DEBUG) startEndTag("<%s />", TAG_SPACER);
    507         XmlParseUtils.checkEndTag(TAG_SPACER, parser);
    508         endKey(spacer);
    509     }
    510 
    511     private void parseIncludeKeyboardContent(final XmlPullParser parser, final boolean skip)
    512             throws XmlPullParserException, IOException {
    513         parseIncludeInternal(parser, null, skip);
    514     }
    515 
    516     private void parseIncludeRowContent(final XmlPullParser parser, final KeyboardRow row,
    517             final boolean skip) throws XmlPullParserException, IOException {
    518         parseIncludeInternal(parser, row, skip);
    519     }
    520 
    521     private void parseIncludeInternal(final XmlPullParser parser, final KeyboardRow row,
    522             final boolean skip) throws XmlPullParserException, IOException {
    523         if (skip) {
    524             XmlParseUtils.checkEndTag(TAG_INCLUDE, parser);
    525             if (DEBUG) startEndTag("</%s> skipped", TAG_INCLUDE);
    526             return;
    527         }
    528         final AttributeSet attr = Xml.asAttributeSet(parser);
    529         final TypedArray keyboardAttr = mResources.obtainAttributes(
    530                 attr, R.styleable.Keyboard_Include);
    531         final TypedArray keyAttr = mResources.obtainAttributes(attr, R.styleable.Keyboard_Key);
    532         int keyboardLayout = 0;
    533         try {
    534             XmlParseUtils.checkAttributeExists(
    535                     keyboardAttr, R.styleable.Keyboard_Include_keyboardLayout, "keyboardLayout",
    536                     TAG_INCLUDE, parser);
    537             keyboardLayout = keyboardAttr.getResourceId(
    538                     R.styleable.Keyboard_Include_keyboardLayout, 0);
    539             if (row != null) {
    540                 // Override current x coordinate.
    541                 row.setXPos(row.getKeyX(keyAttr));
    542                 // Push current Row attributes and update with new attributes.
    543                 row.pushRowAttributes(keyAttr);
    544             }
    545         } finally {
    546             keyboardAttr.recycle();
    547             keyAttr.recycle();
    548         }
    549 
    550         XmlParseUtils.checkEndTag(TAG_INCLUDE, parser);
    551         if (DEBUG) {
    552             startEndTag("<%s keyboardLayout=%s />",TAG_INCLUDE,
    553                     mResources.getResourceEntryName(keyboardLayout));
    554         }
    555         final XmlResourceParser parserForInclude = mResources.getXml(keyboardLayout);
    556         try {
    557             parseMerge(parserForInclude, row, skip);
    558         } finally {
    559             if (row != null) {
    560                 // Restore Row attributes.
    561                 row.popRowAttributes();
    562             }
    563             parserForInclude.close();
    564         }
    565     }
    566 
    567     private void parseMerge(final XmlPullParser parser, final KeyboardRow row, final boolean skip)
    568             throws XmlPullParserException, IOException {
    569         if (DEBUG) startTag("<%s>", TAG_MERGE);
    570         while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
    571             final int event = parser.next();
    572             if (event == XmlPullParser.START_TAG) {
    573                 final String tag = parser.getName();
    574                 if (TAG_MERGE.equals(tag)) {
    575                     if (row == null) {
    576                         parseKeyboardContent(parser, skip);
    577                     } else {
    578                         parseRowContent(parser, row, skip);
    579                     }
    580                     return;
    581                 }
    582                 throw new XmlParseUtils.ParseException(
    583                         "Included keyboard layout must have <merge> root element", parser);
    584             }
    585         }
    586     }
    587 
    588     private void parseSwitchKeyboardContent(final XmlPullParser parser, final boolean skip)
    589             throws XmlPullParserException, IOException {
    590         parseSwitchInternal(parser, null, skip);
    591     }
    592 
    593     private void parseSwitchRowContent(final XmlPullParser parser, final KeyboardRow row,
    594             final boolean skip) throws XmlPullParserException, IOException {
    595         parseSwitchInternal(parser, row, skip);
    596     }
    597 
    598     private void parseSwitchInternal(final XmlPullParser parser, final KeyboardRow row,
    599             final boolean skip) throws XmlPullParserException, IOException {
    600         if (DEBUG) startTag("<%s> %s", TAG_SWITCH, mParams.mId);
    601         boolean selected = false;
    602         while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
    603             final int event = parser.next();
    604             if (event == XmlPullParser.START_TAG) {
    605                 final String tag = parser.getName();
    606                 if (TAG_CASE.equals(tag)) {
    607                     selected |= parseCase(parser, row, selected ? true : skip);
    608                 } else if (TAG_DEFAULT.equals(tag)) {
    609                     selected |= parseDefault(parser, row, selected ? true : skip);
    610                 } else {
    611                     throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_SWITCH);
    612                 }
    613             } else if (event == XmlPullParser.END_TAG) {
    614                 final String tag = parser.getName();
    615                 if (TAG_SWITCH.equals(tag)) {
    616                     if (DEBUG) endTag("</%s>", TAG_SWITCH);
    617                     return;
    618                 }
    619                 throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_SWITCH);
    620             }
    621         }
    622     }
    623 
    624     private boolean parseCase(final XmlPullParser parser, final KeyboardRow row, final boolean skip)
    625             throws XmlPullParserException, IOException {
    626         final boolean selected = parseCaseCondition(parser);
    627         if (row == null) {
    628             // Processing Rows.
    629             parseKeyboardContent(parser, selected ? skip : true);
    630         } else {
    631             // Processing Keys.
    632             parseRowContent(parser, row, selected ? skip : true);
    633         }
    634         return selected;
    635     }
    636 
    637     private boolean parseCaseCondition(final XmlPullParser parser) {
    638         final KeyboardId id = mParams.mId;
    639         if (id == null) {
    640             return true;
    641         }
    642         final AttributeSet attr = Xml.asAttributeSet(parser);
    643         final TypedArray caseAttr = mResources.obtainAttributes(attr, R.styleable.Keyboard_Case);
    644         try {
    645             final boolean keyboardLayoutSetMatched = matchString(caseAttr,
    646                     R.styleable.Keyboard_Case_keyboardLayoutSet,
    647                     id.mSubtype.getKeyboardLayoutSetName());
    648             final boolean keyboardLayoutSetElementMatched = matchTypedValue(caseAttr,
    649                     R.styleable.Keyboard_Case_keyboardLayoutSetElement, id.mElementId,
    650                     KeyboardId.elementIdToName(id.mElementId));
    651             final boolean keyboardThemeMacthed = matchTypedValue(caseAttr,
    652                     R.styleable.Keyboard_Case_keyboardTheme, mParams.mThemeId,
    653                     KeyboardTheme.getKeyboardThemeName(mParams.mThemeId));
    654             final boolean modeMatched = matchTypedValue(caseAttr,
    655                     R.styleable.Keyboard_Case_mode, id.mMode, KeyboardId.modeName(id.mMode));
    656             final boolean navigateNextMatched = matchBoolean(caseAttr,
    657                     R.styleable.Keyboard_Case_navigateNext, id.navigateNext());
    658             final boolean navigatePreviousMatched = matchBoolean(caseAttr,
    659                     R.styleable.Keyboard_Case_navigatePrevious, id.navigatePrevious());
    660             final boolean passwordInputMatched = matchBoolean(caseAttr,
    661                     R.styleable.Keyboard_Case_passwordInput, id.passwordInput());
    662             final boolean clobberSettingsKeyMatched = matchBoolean(caseAttr,
    663                     R.styleable.Keyboard_Case_clobberSettingsKey, id.mClobberSettingsKey);
    664             final boolean hasShortcutKeyMatched = matchBoolean(caseAttr,
    665                     R.styleable.Keyboard_Case_hasShortcutKey, id.mHasShortcutKey);
    666             final boolean languageSwitchKeyEnabledMatched = matchBoolean(caseAttr,
    667                     R.styleable.Keyboard_Case_languageSwitchKeyEnabled,
    668                     id.mLanguageSwitchKeyEnabled);
    669             final boolean isMultiLineMatched = matchBoolean(caseAttr,
    670                     R.styleable.Keyboard_Case_isMultiLine, id.isMultiLine());
    671             final boolean imeActionMatched = matchInteger(caseAttr,
    672                     R.styleable.Keyboard_Case_imeAction, id.imeAction());
    673             final boolean isIconDefinedMatched = isIconDefined(caseAttr,
    674                     R.styleable.Keyboard_Case_isIconDefined, mParams.mIconsSet);
    675             final Locale locale = id.getLocale();
    676             final boolean localeCodeMatched = matchLocaleCodes(caseAttr, locale);
    677             final boolean languageCodeMatched = matchLanguageCodes(caseAttr, locale);
    678             final boolean countryCodeMatched = matchCountryCodes(caseAttr, locale);
    679             final boolean splitLayoutMatched = matchBoolean(caseAttr,
    680                     R.styleable.Keyboard_Case_isSplitLayout, id.mIsSplitLayout);
    681             final boolean selected = keyboardLayoutSetMatched && keyboardLayoutSetElementMatched
    682                     && keyboardThemeMacthed && modeMatched && navigateNextMatched
    683                     && navigatePreviousMatched && passwordInputMatched && clobberSettingsKeyMatched
    684                     && hasShortcutKeyMatched  && languageSwitchKeyEnabledMatched
    685                     && isMultiLineMatched && imeActionMatched && isIconDefinedMatched
    686                     && localeCodeMatched && languageCodeMatched && countryCodeMatched
    687                     && splitLayoutMatched;
    688 
    689             if (DEBUG) {
    690                 startTag("<%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s>%s", TAG_CASE,
    691                         textAttr(caseAttr.getString(
    692                                 R.styleable.Keyboard_Case_keyboardLayoutSet), "keyboardLayoutSet"),
    693                         textAttr(caseAttr.getString(
    694                                 R.styleable.Keyboard_Case_keyboardLayoutSetElement),
    695                                 "keyboardLayoutSetElement"),
    696                         textAttr(caseAttr.getString(
    697                                 R.styleable.Keyboard_Case_keyboardTheme), "keyboardTheme"),
    698                         textAttr(caseAttr.getString(R.styleable.Keyboard_Case_mode), "mode"),
    699                         textAttr(caseAttr.getString(R.styleable.Keyboard_Case_imeAction),
    700                                 "imeAction"),
    701                         booleanAttr(caseAttr, R.styleable.Keyboard_Case_navigateNext,
    702                                 "navigateNext"),
    703                         booleanAttr(caseAttr, R.styleable.Keyboard_Case_navigatePrevious,
    704                                 "navigatePrevious"),
    705                         booleanAttr(caseAttr, R.styleable.Keyboard_Case_clobberSettingsKey,
    706                                 "clobberSettingsKey"),
    707                         booleanAttr(caseAttr, R.styleable.Keyboard_Case_passwordInput,
    708                                 "passwordInput"),
    709                         booleanAttr(caseAttr, R.styleable.Keyboard_Case_hasShortcutKey,
    710                                 "hasShortcutKey"),
    711                         booleanAttr(caseAttr, R.styleable.Keyboard_Case_languageSwitchKeyEnabled,
    712                                 "languageSwitchKeyEnabled"),
    713                         booleanAttr(caseAttr, R.styleable.Keyboard_Case_isMultiLine,
    714                                 "isMultiLine"),
    715                         booleanAttr(caseAttr, R.styleable.Keyboard_Case_isSplitLayout,
    716                                 "splitLayout"),
    717                         textAttr(caseAttr.getString(R.styleable.Keyboard_Case_isIconDefined),
    718                                 "isIconDefined"),
    719                         textAttr(caseAttr.getString(R.styleable.Keyboard_Case_localeCode),
    720                                 "localeCode"),
    721                         textAttr(caseAttr.getString(R.styleable.Keyboard_Case_languageCode),
    722                                 "languageCode"),
    723                         textAttr(caseAttr.getString(R.styleable.Keyboard_Case_countryCode),
    724                                 "countryCode"),
    725                         selected ? "" : " skipped");
    726             }
    727 
    728             return selected;
    729         } finally {
    730             caseAttr.recycle();
    731         }
    732     }
    733 
    734     private static boolean matchLocaleCodes(TypedArray caseAttr, final Locale locale) {
    735         return matchString(caseAttr, R.styleable.Keyboard_Case_localeCode, locale.toString());
    736     }
    737 
    738     private static boolean matchLanguageCodes(TypedArray caseAttr, Locale locale) {
    739         return matchString(caseAttr, R.styleable.Keyboard_Case_languageCode, locale.getLanguage());
    740     }
    741 
    742     private static boolean matchCountryCodes(TypedArray caseAttr, Locale locale) {
    743         return matchString(caseAttr, R.styleable.Keyboard_Case_countryCode, locale.getCountry());
    744     }
    745 
    746     private static boolean matchInteger(final TypedArray a, final int index, final int value) {
    747         // If <case> does not have "index" attribute, that means this <case> is wild-card for
    748         // the attribute.
    749         return !a.hasValue(index) || a.getInt(index, 0) == value;
    750     }
    751 
    752     private static boolean matchBoolean(final TypedArray a, final int index, final boolean value) {
    753         // If <case> does not have "index" attribute, that means this <case> is wild-card for
    754         // the attribute.
    755         return !a.hasValue(index) || a.getBoolean(index, false) == value;
    756     }
    757 
    758     private static boolean matchString(final TypedArray a, final int index, final String value) {
    759         // If <case> does not have "index" attribute, that means this <case> is wild-card for
    760         // the attribute.
    761         return !a.hasValue(index)
    762                 || StringUtils.containsInArray(value, a.getString(index).split("\\|"));
    763     }
    764 
    765     private static boolean matchTypedValue(final TypedArray a, final int index, final int intValue,
    766             final String strValue) {
    767         // If <case> does not have "index" attribute, that means this <case> is wild-card for
    768         // the attribute.
    769         final TypedValue v = a.peekValue(index);
    770         if (v == null) {
    771             return true;
    772         }
    773         if (ResourceUtils.isIntegerValue(v)) {
    774             return intValue == a.getInt(index, 0);
    775         }
    776         if (ResourceUtils.isStringValue(v)) {
    777             return StringUtils.containsInArray(strValue, a.getString(index).split("\\|"));
    778         }
    779         return false;
    780     }
    781 
    782     private static boolean isIconDefined(final TypedArray a, final int index,
    783             final KeyboardIconsSet iconsSet) {
    784         if (!a.hasValue(index)) {
    785             return true;
    786         }
    787         final String iconName = a.getString(index);
    788         final int iconId = KeyboardIconsSet.getIconId(iconName);
    789         return iconsSet.getIconDrawable(iconId) != null;
    790     }
    791 
    792     private boolean parseDefault(final XmlPullParser parser, final KeyboardRow row,
    793             final boolean skip) throws XmlPullParserException, IOException {
    794         if (DEBUG) startTag("<%s>", TAG_DEFAULT);
    795         if (row == null) {
    796             parseKeyboardContent(parser, skip);
    797         } else {
    798             parseRowContent(parser, row, skip);
    799         }
    800         return true;
    801     }
    802 
    803     private void parseKeyStyle(final XmlPullParser parser, final boolean skip)
    804             throws XmlPullParserException, IOException {
    805         final AttributeSet attr = Xml.asAttributeSet(parser);
    806         final TypedArray keyStyleAttr = mResources.obtainAttributes(
    807                 attr, R.styleable.Keyboard_KeyStyle);
    808         final TypedArray keyAttrs = mResources.obtainAttributes(attr, R.styleable.Keyboard_Key);
    809         try {
    810             if (!keyStyleAttr.hasValue(R.styleable.Keyboard_KeyStyle_styleName)) {
    811                 throw new XmlParseUtils.ParseException("<" + TAG_KEY_STYLE
    812                         + "/> needs styleName attribute", parser);
    813             }
    814             if (DEBUG) {
    815                 startEndTag("<%s styleName=%s />%s", TAG_KEY_STYLE,
    816                         keyStyleAttr.getString(R.styleable.Keyboard_KeyStyle_styleName),
    817                         skip ? " skipped" : "");
    818             }
    819             if (!skip) {
    820                 mParams.mKeyStyles.parseKeyStyleAttributes(keyStyleAttr, keyAttrs, parser);
    821             }
    822         } finally {
    823             keyStyleAttr.recycle();
    824             keyAttrs.recycle();
    825         }
    826         XmlParseUtils.checkEndTag(TAG_KEY_STYLE, parser);
    827     }
    828 
    829     private void startKeyboard() {
    830         mCurrentY += mParams.mTopPadding;
    831         mTopEdge = true;
    832     }
    833 
    834     private void startRow(final KeyboardRow row) {
    835         addEdgeSpace(mParams.mLeftPadding, row);
    836         mCurrentRow = row;
    837         mLeftEdge = true;
    838         mRightEdgeKey = null;
    839     }
    840 
    841     private void endRow(final KeyboardRow row) {
    842         if (mCurrentRow == null) {
    843             throw new RuntimeException("orphan end row tag");
    844         }
    845         if (mRightEdgeKey != null) {
    846             mRightEdgeKey.markAsRightEdge(mParams);
    847             mRightEdgeKey = null;
    848         }
    849         addEdgeSpace(mParams.mRightPadding, row);
    850         mCurrentY += row.getRowHeight();
    851         mCurrentRow = null;
    852         mTopEdge = false;
    853     }
    854 
    855     private void endKey(@Nonnull final Key key) {
    856         mParams.onAddKey(key);
    857         if (mLeftEdge) {
    858             key.markAsLeftEdge(mParams);
    859             mLeftEdge = false;
    860         }
    861         if (mTopEdge) {
    862             key.markAsTopEdge(mParams);
    863         }
    864         mRightEdgeKey = key;
    865     }
    866 
    867     private void endKeyboard() {
    868         mParams.removeRedundantMoreKeys();
    869         // {@link #parseGridRows(XmlPullParser,boolean)} may populate keyboard rows higher than
    870         // previously expected.
    871         final int actualHeight = mCurrentY - mParams.mVerticalGap + mParams.mBottomPadding;
    872         mParams.mOccupiedHeight = Math.max(mParams.mOccupiedHeight, actualHeight);
    873     }
    874 
    875     private void addEdgeSpace(final float width, final KeyboardRow row) {
    876         row.advanceXPos(width);
    877         mLeftEdge = false;
    878         mRightEdgeKey = null;
    879     }
    880 
    881     private static String textAttr(final String value, final String name) {
    882         return value != null ? String.format(" %s=%s", name, value) : "";
    883     }
    884 
    885     private static String booleanAttr(final TypedArray a, final int index, final String name) {
    886         return a.hasValue(index)
    887                 ? String.format(" %s=%s", name, a.getBoolean(index, false)) : "";
    888     }
    889 }
    890