content: /*
   * Copyright (C) 2012 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package com.amnesica.kryptey.inputmethod.keyboard.internal;
  
  import android.content.Context;
  import android.content.res.Resources;
  import android.content.res.TypedArray;
  import android.content.res.XmlResourceParser;
  import android.text.TextUtils;
  import android.util.AttributeSet;
  import android.util.Log;
  import android.util.TypedValue;
  import android.util.Xml;
  
  import com.amnesica.kryptey.inputmethod.R;
  import com.amnesica.kryptey.inputmethod.keyboard.Key;
  import com.amnesica.kryptey.inputmethod.keyboard.Keyboard;
  import com.amnesica.kryptey.inputmethod.keyboard.KeyboardId;
  import com.amnesica.kryptey.inputmethod.keyboard.KeyboardTheme;
  import com.amnesica.kryptey.inputmethod.latin.common.StringUtils;
  import com.amnesica.kryptey.inputmethod.latin.utils.ResourceUtils;
  import com.amnesica.kryptey.inputmethod.latin.utils.XmlParseUtils;
  import com.amnesica.kryptey.inputmethod.latin.utils.XmlParseUtils.ParseException;
  
  import org.xmlpull.v1.XmlPullParser;
  import org.xmlpull.v1.XmlPullParserException;
  
  import java.io.IOException;
  import java.util.Arrays;
  import java.util.Locale;
  
  /**
   * Keyboard Building helper.
   * <p>
   * This class parses Keyboard XML file and eventually build a Keyboard.
   * The Keyboard XML file looks like:
   * <pre>
   *   &lt;!-- xml/keyboard.xml --&gt;
   *   &lt;Keyboard keyboard_attributes*&gt;
   *     &lt;!-- Keyboard Content --&gt;
   *     &lt;Row row_attributes*&gt;
   *       &lt;!-- Row Content --&gt;
   *       &lt;Key key_attributes* /&gt;
   *       &lt;Spacer horizontalGap="32.0dp" /&gt;
   *       &lt;include keyboardLayout="@xml/other_keys"&gt;
   *       ...
   *     &lt;/Row&gt;
   *     &lt;include keyboardLayout="@xml/other_rows"&gt;
   *     ...
   *   &lt;/Keyboard&gt;
   * </pre>
   * The XML file which is included in other file must have &lt;merge&gt; as root element,
   * such as:
   * <pre>
   *   &lt;!-- xml/other_keys.xml --&gt;
   *   &lt;merge&gt;
   *     &lt;Key key_attributes* /&gt;
   *     ...
   *   &lt;/merge&gt;
   * </pre>
   * and
   * <pre>
   *   &lt;!-- xml/other_rows.xml --&gt;
   *   &lt;merge&gt;
   *     &lt;Row row_attributes*&gt;
   *       &lt;Key key_attributes* /&gt;
   *     &lt;/Row&gt;
   *     ...
   *   &lt;/merge&gt;
   * </pre>
   * You can also use switch-case-default tags to select Rows and Keys.
   * <pre>
   *   &lt;switch&gt;
   *     &lt;case case_attribute*&gt;
   *       &lt;!-- Any valid tags at switch position --&gt;
   *     &lt;/case&gt;
   *     ...
   *     &lt;default&gt;
   *       &lt;!-- Any valid tags at switch position --&gt;
   *     &lt;/default&gt;
   *   &lt;/switch&gt;
   * </pre>
   * You can declare Key style and specify styles within Key tags.
   * <pre>
   *     &lt;switch&gt;
   *       &lt;case mode="email"&gt;
   *         &lt;key-style styleName="f1-key" parentStyle="modifier-key"
   *           keyLabel=".com"
   *         /&gt;
   *       &lt;/case&gt;
   *       &lt;case mode="url"&gt;
   *         &lt;key-style styleName="f1-key" parentStyle="modifier-key"
   *           keyLabel="http://"
   *         /&gt;
   *       &lt;/case&gt;
   *     &lt;/switch&gt;
   *     ...
   *     &lt;Key keyStyle="shift-key" ... /&gt;
   * </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;
  
    // Keyboard XML Tags
    private static final String TAG_KEYBOARD = "Keyboard";
    private static final String TAG_ROW = "Row";
    private static final String TAG_KEY = "Key";
    private static final String TAG_SPACER = "Spacer";
    private static final String TAG_INCLUDE = "include";
    private static final String TAG_MERGE = "merge";
    private static final String TAG_SWITCH = "switch";
    private static final String TAG_CASE = "case";
    private static final String TAG_DEFAULT = "default";
    public static final String TAG_KEY_STYLE = "key-style";
  
    private static final int DEFAULT_KEYBOARD_COLUMNS = 10;
    private static final int DEFAULT_KEYBOARD_ROWS = 4;
  
    protected final KP mParams;
    protected final Context mContext;
    protected final Resources mResources;
  
    private float mCurrentY = 0;
    private KeyboardRow mCurrentRow = null;
    private Key mPreviousKeyInRow = null;
    private boolean mKeyboardDefined = false;
  
    public KeyboardBuilder(final Context context, final KP params) {
      mContext = context;
      final Resources res = context.getResources();
      mResources = res;
  
      mParams = params;
  
      params.mGridWidth = res.getInteger(R.integer.config_keyboard_grid_width);
      params.mGridHeight = res.getInteger(R.integer.config_keyboard_grid_height);
    }
  
    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);
      try {
        parseKeyboard(parser, false);
        if (!mKeyboardDefined) {
          throw new XmlParseUtils.ParseException("No " + TAG_KEYBOARD + " tag was found");
        }
      } catch (XmlPullParserException e) {
        Log.w(BUILDER_TAG, "keyboard XML parse error", e);
        throw new IllegalArgumentException(e.getMessage(), e);
      } catch (IOException e) {
        Log.w(BUILDER_TAG, "keyboard XML parse error", e);
        throw new RuntimeException(e.getMessage(), e);
      } finally {
        parser.close();
      }
      return this;
    }
  
    public Keyboard build() {
      return new Keyboard(mParams);
    }
  
    private int mIndent;
    private static final String SPACES = "                                             ";
  
    private static String spaces(final int count) {
      return (count < SPACES.length()) ? SPACES.substring(0, count) : SPACES;
    }
  
    private void startTag(final String format, final Object... args) {
      Log.d(BUILDER_TAG, String.format(spaces(++mIndent * 2) + format, args));
    }
  
    private void endTag(final String format, final Object... args) {
      Log.d(BUILDER_TAG, String.format(spaces(mIndent-- * 2) + format, args));
    }
  
    private void startEndTag(final String format, final Object... args) {
      Log.d(BUILDER_TAG, String.format(spaces(++mIndent * 2) + format, args));
      mIndent--;
    }
  
    private void parseKeyboard(final XmlPullParser parser, final boolean skip)
        throws XmlPullParserException, IOException {
      while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
        final int event = parser.next();
        if (event == XmlPullParser.START_TAG) {
          final String tag = parser.getName();
          if (TAG_KEYBOARD.equals(tag)) {
            if (DEBUG) startTag("<%s> %s%s", TAG_KEYBOARD, mParams.mId,
                skip ? " skipped" : "");
            if (!skip) {
              if (mKeyboardDefined) {
                throw new XmlParseUtils.ParseException("Only one " + TAG_KEYBOARD
                    + " tag can be defined", parser);
              }
              mKeyboardDefined = true;
              parseKeyboardAttributes(parser);
              startKeyboard();
            }
            parseKeyboardContent(parser, skip);
          } else if (TAG_SWITCH.equals(tag)) {
            parseSwitchKeyboard(parser, skip);
          } else {
            throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD);
          }
        } else if (event == XmlPullParser.END_TAG) {
          final String tag = parser.getName();
          if (DEBUG) endTag("</%s>", tag);
          if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag)) {
            return;
          }
          throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_ROW);
        }
      }
    }
  
    private void parseKeyboardAttributes(final XmlPullParser parser) {
      final AttributeSet attr = Xml.asAttributeSet(parser);
      final TypedArray keyboardAttr = mContext.obtainStyledAttributes(
          attr, R.styleable.Keyboard, R.attr.keyboardStyle, R.style.Keyboard);
      final TypedArray keyAttr = mResources.obtainAttributes(attr, R.styleable.Keyboard_Key);
      try {
        final KeyboardParams params = mParams;
        final int height = params.mId.mHeight;
        final int width = params.mId.mWidth;
        // The bonus height isn't used to determine the other dimensions (gap/padding) to allow
        // those to stay consistent between layouts with and without the bonus height added.
        final int bonusHeight = (int) keyboardAttr.getFraction(R.styleable.Keyboard_bonusHeight,
            height, height, 0);
        params.mOccupiedHeight = height + bonusHeight;
        params.mOccupiedWidth = width;
        params.mTopPadding = ResourceUtils.getDimensionOrFraction(keyboardAttr,
            R.styleable.Keyboard_keyboardTopPadding, height, 0);
        params.mBottomPadding = ResourceUtils.getDimensionOrFraction(keyboardAttr,
            R.styleable.Keyboard_keyboardBottomPadding, height, 0);
        params.mLeftPadding = ResourceUtils.getDimensionOrFraction(keyboardAttr,
            R.styleable.Keyboard_keyboardLeftPadding, width, 0);
        params.mRightPadding = ResourceUtils.getDimensionOrFraction(keyboardAttr,
            R.styleable.Keyboard_keyboardRightPadding, width, 0);
  
        params.mHorizontalGap = keyboardAttr.getFraction(
            R.styleable.Keyboard_horizontalGap, width, width, 0);
        final float baseWidth = params.mOccupiedWidth - params.mLeftPadding
            - params.mRightPadding + params.mHorizontalGap;
        params.mBaseWidth = baseWidth;
        params.mDefaultKeyPaddedWidth = ResourceUtils.getFraction(keyAttr,
            R.styleable.Keyboard_Key_keyWidth, baseWidth,
            baseWidth / DEFAULT_KEYBOARD_COLUMNS);
        // TODO: Fix keyboard geometry calculation clearer. Historically vertical gap between
        // rows are determined based on the entire keyboard height including top and bottom
        // paddings.
        params.mVerticalGap = keyboardAttr.getFraction(
            R.styleable.Keyboard_verticalGap, height, height, 0);
        final float baseHeight = params.mOccupiedHeight - params.mTopPadding
            - params.mBottomPadding + params.mVerticalGap;
        params.mBaseHeight = baseHeight;
        params.mDefaultRowHeight = ResourceUtils.getDimensionOrFraction(keyboardAttr,
            R.styleable.Keyboard_rowHeight, baseHeight, baseHeight / DEFAULT_KEYBOARD_ROWS);
  
        params.mKeyVisualAttributes = KeyVisualAttributes.newInstance(keyAttr);
  
        params.mMoreKeysTemplate = keyboardAttr.getResourceId(
            R.styleable.Keyboard_moreKeysTemplate, 0);
        params.mMaxMoreKeysKeyboardColumn = keyAttr.getInt(
            R.styleable.Keyboard_Key_maxMoreKeysColumn, 5);
  
        params.mIconsSet.loadIcons(keyboardAttr);
        params.mTextsSet.setLocale(params.mId.getLocale(), mContext);
      } finally {
        keyAttr.recycle();
        keyboardAttr.recycle();
      }
    }
  
    private void parseKeyboardContent(final XmlPullParser parser, final boolean skip)
        throws XmlPullParserException, IOException {
      while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
        final int event = parser.next();
        if (event == XmlPullParser.START_TAG) {
          final String tag = parser.getName();
          if (TAG_ROW.equals(tag)) {
            final KeyboardRow row = parseRowAttributes(parser);
            if (DEBUG) startTag("<%s>%s", TAG_ROW, skip ? " skipped" : "");
            if (!skip) {
              startRow(row);
            }
            parseRowContent(parser, row, skip);
          } else if (TAG_INCLUDE.equals(tag)) {
            parseIncludeKeyboardContent(parser, skip);
          } else if (TAG_SWITCH.equals(tag)) {
            parseSwitchKeyboardContent(parser, skip);
          } else if (TAG_KEY_STYLE.equals(tag)) {
            parseKeyStyle(parser, skip);
          } else {
            throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_ROW);
          }
        } else if (event == XmlPullParser.END_TAG) {
          final String tag = parser.getName();
          if (DEBUG) endTag("</%s>", tag);
          if (TAG_KEYBOARD.equals(tag)) {
            endKeyboard();
            return;
          }
          if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag) || TAG_MERGE.equals(tag)) {
            return;
          }
          throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_ROW);
        }
      }
    }
  
    private KeyboardRow parseRowAttributes(final XmlPullParser parser)
        throws XmlPullParserException {
      final AttributeSet attr = Xml.asAttributeSet(parser);
      final TypedArray keyboardAttr = mResources.obtainAttributes(attr, R.styleable.Keyboard);
      try {
        if (keyboardAttr.hasValue(R.styleable.Keyboard_horizontalGap)) {
          throw new XmlParseUtils.IllegalAttribute(parser, TAG_ROW, "horizontalGap");
        }
        if (keyboardAttr.hasValue(R.styleable.Keyboard_verticalGap)) {
          throw new XmlParseUtils.IllegalAttribute(parser, TAG_ROW, "verticalGap");
        }
        return new KeyboardRow(mResources, mParams, parser, mCurrentY);
      } finally {
        keyboardAttr.recycle();
      }
    }
  
    private void parseRowContent(final XmlPullParser parser, final KeyboardRow row,
                                 final boolean skip) throws XmlPullParserException, IOException {
      while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
        final int event = parser.next();
        if (event == XmlPullParser.START_TAG) {
          final String tag = parser.getName();
          if (TAG_KEY.equals(tag)) {
            parseKey(parser, row, skip);
          } else if (TAG_SPACER.equals(tag)) {
            parseSpacer(parser, row, skip);
          } else if (TAG_INCLUDE.equals(tag)) {
            parseIncludeRowContent(parser, row, skip);
          } else if (TAG_SWITCH.equals(tag)) {
            parseSwitchRowContent(parser, row, skip);
          } else if (TAG_KEY_STYLE.equals(tag)) {
            parseKeyStyle(parser, skip);
          } else {
            throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_ROW);
          }
        } else if (event == XmlPullParser.END_TAG) {
          final String tag = parser.getName();
          if (DEBUG) endTag("</%s>", tag);
          if (TAG_ROW.equals(tag)) {
            if (!skip) {
              endRow(row);
            }
            return;
          }
          if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag) || TAG_MERGE.equals(tag)) {
            return;
          }
          throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_ROW);
        }
      }
    }
  
    private void parseKey(final XmlPullParser parser, final KeyboardRow row, final boolean skip)
        throws XmlPullParserException, IOException {
      if (skip) {
        XmlParseUtils.checkEndTag(TAG_KEY, parser);
        if (DEBUG) startEndTag("<%s /> skipped", TAG_KEY);
        return;
      }
      final TypedArray keyAttr = mResources.obtainAttributes(
          Xml.asAttributeSet(parser), R.styleable.Keyboard_Key);
      final KeyStyle keyStyle = mParams.mKeyStyles.getKeyStyle(keyAttr, parser);
      final String keySpec = keyStyle.getString(keyAttr, R.styleable.Keyboard_Key_keySpec);
      if (TextUtils.isEmpty(keySpec)) {
        throw new ParseException("Empty keySpec", parser);
      }
      final Key key = new Key(keySpec, keyAttr, keyStyle, mParams, row);
      keyAttr.recycle();
      if (DEBUG) {
        startEndTag("<%s %s moreKeys=%s />", TAG_KEY, key, Arrays.toString(key.getMoreKeys()));
      }
      XmlParseUtils.checkEndTag(TAG_KEY, parser);
      endKey(key, row);
    }
  
    private void parseSpacer(final XmlPullParser parser, final KeyboardRow row, final boolean skip)
        throws XmlPullParserException, IOException {
      if (skip) {
        XmlParseUtils.checkEndTag(TAG_SPACER, parser);
        if (DEBUG) startEndTag("<%s /> skipped", TAG_SPACER);
        return;
      }
      final TypedArray keyAttr = mResources.obtainAttributes(
          Xml.asAttributeSet(parser), R.styleable.Keyboard_Key);
      final KeyStyle keyStyle = mParams.mKeyStyles.getKeyStyle(keyAttr, parser);
      final Key spacer = new Key.Spacer(keyAttr, keyStyle, mParams, row);
      keyAttr.recycle();
      if (DEBUG) startEndTag("<%s />", TAG_SPACER);
      XmlParseUtils.checkEndTag(TAG_SPACER, parser);
      endKey(spacer, row);
    }
  
    private void parseIncludeKeyboardContent(final XmlPullParser parser, final boolean skip)
        throws XmlPullParserException, IOException {
      parseIncludeInternal(parser, null, skip);
    }
  
    private void parseIncludeRowContent(final XmlPullParser parser, final KeyboardRow row,
                                        final boolean skip) throws XmlPullParserException, IOException {
      parseIncludeInternal(parser, row, skip);
    }
  
    private void parseIncludeInternal(final XmlPullParser parser, final KeyboardRow row,
                                      final boolean skip) throws XmlPullParserException, IOException {
      if (skip) {
        XmlParseUtils.checkEndTag(TAG_INCLUDE, parser);
        if (DEBUG) startEndTag("</%s> skipped", TAG_INCLUDE);
        return;
      }
      final AttributeSet attr = Xml.asAttributeSet(parser);
      final TypedArray keyboardAttr = mResources.obtainAttributes(
          attr, R.styleable.Keyboard_Include);
      final TypedArray includeAttr = mResources.obtainAttributes(
          attr, R.styleable.Keyboard);
      mParams.mDefaultRowHeight = ResourceUtils.getDimensionOrFraction(includeAttr,
          R.styleable.Keyboard_rowHeight, mParams.mBaseHeight, mParams.mDefaultRowHeight);
  
      final TypedArray keyAttr = mResources.obtainAttributes(attr, R.styleable.Keyboard_Key);
      int keyboardLayout = 0;
      try {
        XmlParseUtils.checkAttributeExists(
            keyboardAttr, R.styleable.Keyboard_Include_keyboardLayout, "keyboardLayout",
            TAG_INCLUDE, parser);
        keyboardLayout = keyboardAttr.getResourceId(
            R.styleable.Keyboard_Include_keyboardLayout, 0);
        if (row != null) {
          // Override current x coordinate.
          row.updateXPos(keyAttr);
          // Push current Row attributes and update with new attributes.
          row.pushRowAttributes(keyAttr);
        }
      } finally {
        keyboardAttr.recycle();
        keyAttr.recycle();
        includeAttr.recycle();
      }
  
      XmlParseUtils.checkEndTag(TAG_INCLUDE, parser);
      if (DEBUG) {
        startEndTag("<%s keyboardLayout=%s />", TAG_INCLUDE,
            mResources.getResourceEntryName(keyboardLayout));
      }
      final XmlResourceParser parserForInclude = mResources.getXml(keyboardLayout);
      try {
        parseMerge(parserForInclude, row, skip);
      } finally {
        if (row != null) {
          // Restore Row attributes.
          row.popRowAttributes();
        }
        parserForInclude.close();
      }
    }
  
    private void parseMerge(final XmlPullParser parser, final KeyboardRow row, final boolean skip)
        throws XmlPullParserException, IOException {
      if (DEBUG) startTag("<%s>", TAG_MERGE);
      while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
        final int event = parser.next();
        if (event == XmlPullParser.START_TAG) {
          final String tag = parser.getName();
          if (TAG_MERGE.equals(tag)) {
            if (row == null) {
              parseKeyboardContent(parser, skip);
            } else {
              parseRowContent(parser, row, skip);
            }
            return;
          }
          throw new XmlParseUtils.ParseException(
              "Included keyboard layout must have <merge> root element", parser);
        }
      }
    }
  
    private void parseSwitchKeyboard(final XmlPullParser parser, final boolean skip)
        throws XmlPullParserException, IOException {
      parseSwitchInternal(parser, true, null, skip);
    }
  
    private void parseSwitchKeyboardContent(final XmlPullParser parser, final boolean skip)
        throws XmlPullParserException, IOException {
      parseSwitchInternal(parser, false, null, skip);
    }
  
    private void parseSwitchRowContent(final XmlPullParser parser, final KeyboardRow row,
                                       final boolean skip) throws XmlPullParserException, IOException {
      parseSwitchInternal(parser, false, row, skip);
    }
  
    private void parseSwitchInternal(final XmlPullParser parser, final boolean parseKeyboard,
                                     final KeyboardRow row, final boolean skip) throws XmlPullParserException, IOException {
      if (DEBUG) startTag("<%s> %s", TAG_SWITCH, mParams.mId);
      boolean selected = false;
      while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
        final int event = parser.next();
        if (event == XmlPullParser.START_TAG) {
          final String tag = parser.getName();
          if (TAG_CASE.equals(tag)) {
            selected |= parseCase(parser, parseKeyboard, row, selected || skip);
          } else if (TAG_DEFAULT.equals(tag)) {
            selected |= parseDefault(parser, parseKeyboard, row, selected || skip);
          } else {
            throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_SWITCH);
          }
        } else if (event == XmlPullParser.END_TAG) {
          final String tag = parser.getName();
          if (TAG_SWITCH.equals(tag)) {
            if (DEBUG) endTag("</%s>", TAG_SWITCH);
            return;
          }
          throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_SWITCH);
        }
      }
    }
  
    private boolean parseCase(final XmlPullParser parser, final boolean parseKeyboard,
                              final KeyboardRow row, final boolean skip) throws XmlPullParserException, IOException {
      final boolean selected = parseCaseCondition(parser);
      if (parseKeyboard) {
        // Processing Keyboard root.
        parseKeyboard(parser, !selected || skip);
      } else if (row == null) {
        // Processing Rows.
        parseKeyboardContent(parser, !selected || skip);
      } else {
        // Processing Keys.
        parseRowContent(parser, row, !selected || skip);
      }
      return selected;
    }
  
    private boolean parseCaseCondition(final XmlPullParser parser) {
      final KeyboardId id = mParams.mId;
      if (id == null) {
        return true;
      }
      final AttributeSet attr = Xml.asAttributeSet(parser);
      final TypedArray caseAttr = mResources.obtainAttributes(attr, R.styleable.Keyboard_Case);
      if (DEBUG) startTag("<%s>", TAG_CASE);
      try {
        final boolean keyboardLayoutSetMatched = matchString(caseAttr,
            R.styleable.Keyboard_Case_keyboardLayoutSet,
            id.mSubtype.getKeyboardLayoutSet());
        final boolean keyboardLayoutSetElementMatched = matchTypedValue(caseAttr,
            R.styleable.Keyboard_Case_keyboardLayoutSetElement, id.mElementId,
            KeyboardId.elementIdToName(id.mElementId));
        final boolean keyboardThemeMatched = matchTypedValue(caseAttr,
            R.styleable.Keyboard_Case_keyboardTheme, id.mThemeId,
            KeyboardTheme.getKeyboardThemeName(id.mThemeId));
        final boolean modeMatched = matchTypedValue(caseAttr,
            R.styleable.Keyboard_Case_mode, id.mMode, KeyboardId.modeName(id.mMode));
        final boolean navigateNextMatched = matchBoolean(caseAttr,
            R.styleable.Keyboard_Case_navigateNext, id.navigateNext());
        final boolean navigatePreviousMatched = matchBoolean(caseAttr,
            R.styleable.Keyboard_Case_navigatePrevious, id.navigatePrevious());
        final boolean passwordInputMatched = matchBoolean(caseAttr,
            R.styleable.Keyboard_Case_passwordInput, id.passwordInput());
        final boolean languageSwitchKeyEnabledMatched = matchBoolean(caseAttr,
            R.styleable.Keyboard_Case_languageSwitchKeyEnabled,
            id.mLanguageSwitchKeyEnabled);
        final boolean isMultiLineMatched = matchBoolean(caseAttr,
            R.styleable.Keyboard_Case_isMultiLine, id.isMultiLine());
        final boolean imeActionMatched = matchInteger(caseAttr,
            R.styleable.Keyboard_Case_imeAction, id.imeAction());
        final boolean isIconDefinedMatched = isIconDefined(caseAttr,
            R.styleable.Keyboard_Case_isIconDefined, mParams.mIconsSet);
        final Locale locale = id.getLocale();
        final boolean localeCodeMatched = matchLocaleCodes(caseAttr, locale);
        final boolean languageCodeMatched = matchLanguageCodes(caseAttr, locale);
        final boolean countryCodeMatched = matchCountryCodes(caseAttr, locale);
        final boolean showMoreKeysMatched = matchBoolean(caseAttr,
            R.styleable.Keyboard_Case_showExtraChars, id.mShowMoreKeys);
        final boolean showNumberRowMatched = matchBoolean(caseAttr,
            R.styleable.Keyboard_Case_showNumberRow, id.mShowNumberRow);
        final boolean selected = keyboardLayoutSetMatched && keyboardLayoutSetElementMatched
            && keyboardThemeMatched && modeMatched && navigateNextMatched
            && navigatePreviousMatched && passwordInputMatched
            && languageSwitchKeyEnabledMatched
            && isMultiLineMatched && imeActionMatched && isIconDefinedMatched
            && localeCodeMatched && languageCodeMatched && countryCodeMatched
            && showMoreKeysMatched && showNumberRowMatched;
  
        return selected;
      } finally {
        caseAttr.recycle();
      }
    }
  
    private static boolean matchLocaleCodes(TypedArray caseAttr, final Locale locale) {
      return matchString(caseAttr, R.styleable.Keyboard_Case_localeCode, locale.toString());
    }
  
    private static boolean matchLanguageCodes(TypedArray caseAttr, Locale locale) {
      return matchString(caseAttr, R.styleable.Keyboard_Case_languageCode, locale.getLanguage());
    }
  
    private static boolean matchCountryCodes(TypedArray caseAttr, Locale locale) {
      return matchString(caseAttr, R.styleable.Keyboard_Case_countryCode, locale.getCountry());
    }
  
    private static boolean matchInteger(final TypedArray a, final int index, final int value) {
      // If <case> does not have "index" attribute, that means this <case> is wild-card for
      // the attribute.
      return !a.hasValue(index) || a.getInt(index, 0) == value;
    }
  
    private static boolean matchBoolean(final TypedArray a, final int index, final boolean value) {
      // If <case> does not have "index" attribute, that means this <case> is wild-card for
      // the attribute.
      return !a.hasValue(index) || a.getBoolean(index, false) == value;
    }
  
    private static boolean matchString(final TypedArray a, final int index, final String value) {
      // If <case> does not have "index" attribute, that means this <case> is wild-card for
      // the attribute.
      return !a.hasValue(index)
          || StringUtils.containsInArray(value, a.getString(index).split("\\|"));
    }
  
    private static boolean matchTypedValue(final TypedArray a, final int index, final int intValue,
                                           final String strValue) {
      // If <case> does not have "index" attribute, that means this <case> is wild-card for
      // the attribute.
      final TypedValue v = a.peekValue(index);
      if (v == null) {
        return true;
      }
      if (ResourceUtils.isIntegerValue(v)) {
        return intValue == a.getInt(index, 0);
      }
      if (ResourceUtils.isStringValue(v)) {
        return StringUtils.containsInArray(strValue, a.getString(index).split("\\|"));
      }
      return false;
    }
  
    private static boolean isIconDefined(final TypedArray a, final int index,
                                         final KeyboardIconsSet iconsSet) {
      if (!a.hasValue(index)) {
        return true;
      }
      final String iconName = a.getString(index);
      final int iconId = KeyboardIconsSet.getIconId(iconName);
      return iconsSet.getIconDrawable(iconId) != null;
    }
  
    private boolean parseDefault(final XmlPullParser parser, final boolean parseKeyboard,
                                 final KeyboardRow row, final boolean skip) throws XmlPullParserException, IOException {
      if (DEBUG) startTag("<%s>", TAG_DEFAULT);
      if (parseKeyboard) {
        parseKeyboard(parser, skip);
      } else if (row == null) {
        parseKeyboardContent(parser, skip);
      } else {
        parseRowContent(parser, row, skip);
      }
      return true;
    }
  
    private void parseKeyStyle(final XmlPullParser parser, final boolean skip)
        throws XmlPullParserException, IOException {
      final AttributeSet attr = Xml.asAttributeSet(parser);
      final TypedArray keyStyleAttr = mResources.obtainAttributes(
          attr, R.styleable.Keyboard_KeyStyle);
      final TypedArray keyAttrs = mResources.obtainAttributes(attr, R.styleable.Keyboard_Key);
      try {
        if (!keyStyleAttr.hasValue(R.styleable.Keyboard_KeyStyle_styleName)) {
          throw new XmlParseUtils.ParseException("<" + TAG_KEY_STYLE
              + "/> needs styleName attribute", parser);
        }
        if (DEBUG) {
          startEndTag("<%s styleName=%s />%s", TAG_KEY_STYLE,
              keyStyleAttr.getString(R.styleable.Keyboard_KeyStyle_styleName),
              skip ? " skipped" : "");
        }
        if (!skip) {
          mParams.mKeyStyles.parseKeyStyleAttributes(keyStyleAttr, keyAttrs, parser);
        }
      } finally {
        keyStyleAttr.recycle();
        keyAttrs.recycle();
      }
      XmlParseUtils.checkEndTag(TAG_KEY_STYLE, parser);
    }
  
    private void startKeyboard() {
  
    }
  
    private void startRow(final KeyboardRow row) {
      mCurrentRow = row;
      mPreviousKeyInRow = null;
    }
  
    private void endRow(final KeyboardRow row) {
      if (mCurrentRow == null) {
        throw new RuntimeException("orphan end row tag");
      }
      if (mPreviousKeyInRow != null && !mPreviousKeyInRow.isSpacer()) {
        setKeyHitboxRightEdge(mPreviousKeyInRow, mParams.mOccupiedWidth);
        mPreviousKeyInRow = null;
      }
      mCurrentY += row.getRowHeight();
      mCurrentRow = null;
    }
  
    private void endKey(final Key key, final KeyboardRow row) {
      mParams.onAddKey(key);
      if (mPreviousKeyInRow != null && !mPreviousKeyInRow.isSpacer()) {
        // Make the last key span the gap so there isn't un-clickable space. The current key's
        // hitbox left edge is based on the previous key, so this will make the gap between
        // them split evenly.
        setKeyHitboxRightEdge(mPreviousKeyInRow, row.getKeyX() - row.getKeyLeftPadding());
      }
      mPreviousKeyInRow = key;
    }
  
    private void setKeyHitboxRightEdge(final Key key, final float xPos) {
      final int keyRight = key.getX() + key.getWidth();
      final float padding = xPos - keyRight;
      key.setHitboxRightEdge(Math.round(padding) + keyRight);
    }
  
    private void endKeyboard() {
      mParams.removeRedundantMoreKeys();
    }
  }
  