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.util.AttributeSet;
     24 import android.util.Log;
     25 import android.util.TypedValue;
     26 import android.util.Xml;
     27 
     28 import com.android.inputmethod.annotations.UsedForTesting;
     29 import com.android.inputmethod.keyboard.Key;
     30 import com.android.inputmethod.keyboard.Keyboard;
     31 import com.android.inputmethod.keyboard.KeyboardId;
     32 import com.android.inputmethod.latin.LocaleUtils.RunInLocale;
     33 import com.android.inputmethod.latin.R;
     34 import com.android.inputmethod.latin.ResourceUtils;
     35 import com.android.inputmethod.latin.StringUtils;
     36 import com.android.inputmethod.latin.SubtypeLocale;
     37 import com.android.inputmethod.latin.XmlParseUtils;
     38 
     39 import org.xmlpull.v1.XmlPullParser;
     40 import org.xmlpull.v1.XmlPullParserException;
     41 
     42 import java.io.IOException;
     43 import java.util.Arrays;
     44 import java.util.Locale;
     45 
     46 /**
     47  * Keyboard Building helper.
     48  *
     49  * This class parses Keyboard XML file and eventually build a Keyboard.
     50  * The Keyboard XML file looks like:
     51  * <pre>
     52  *   &lt;!-- xml/keyboard.xml --&gt;
     53  *   &lt;Keyboard keyboard_attributes*&gt;
     54  *     &lt;!-- Keyboard Content --&gt;
     55  *     &lt;Row row_attributes*&gt;
     56  *       &lt;!-- Row Content --&gt;
     57  *       &lt;Key key_attributes* /&gt;
     58  *       &lt;Spacer horizontalGap="32.0dp" /&gt;
     59  *       &lt;include keyboardLayout="@xml/other_keys"&gt;
     60  *       ...
     61  *     &lt;/Row&gt;
     62  *     &lt;include keyboardLayout="@xml/other_rows"&gt;
     63  *     ...
     64  *   &lt;/Keyboard&gt;
     65  * </pre>
     66  * The XML file which is included in other file must have &lt;merge&gt; as root element,
     67  * such as:
     68  * <pre>
     69  *   &lt;!-- xml/other_keys.xml --&gt;
     70  *   &lt;merge&gt;
     71  *     &lt;Key key_attributes* /&gt;
     72  *     ...
     73  *   &lt;/merge&gt;
     74  * </pre>
     75  * and
     76  * <pre>
     77  *   &lt;!-- xml/other_rows.xml --&gt;
     78  *   &lt;merge&gt;
     79  *     &lt;Row row_attributes*&gt;
     80  *       &lt;Key key_attributes* /&gt;
     81  *     &lt;/Row&gt;
     82  *     ...
     83  *   &lt;/merge&gt;
     84  * </pre>
     85  * You can also use switch-case-default tags to select Rows and Keys.
     86  * <pre>
     87  *   &lt;switch&gt;
     88  *     &lt;case case_attribute*&gt;
     89  *       &lt;!-- Any valid tags at switch position --&gt;
     90  *     &lt;/case&gt;
     91  *     ...
     92  *     &lt;default&gt;
     93  *       &lt;!-- Any valid tags at switch position --&gt;
     94  *     &lt;/default&gt;
     95  *   &lt;/switch&gt;
     96  * </pre>
     97  * You can declare Key style and specify styles within Key tags.
     98  * <pre>
     99  *     &lt;switch&gt;
    100  *       &lt;case mode="email"&gt;
    101  *         &lt;key-style styleName="f1-key" parentStyle="modifier-key"
    102  *           keyLabel=".com"
    103  *         /&gt;
    104  *       &lt;/case&gt;
    105  *       &lt;case mode="url"&gt;
    106  *         &lt;key-style styleName="f1-key" parentStyle="modifier-key"
    107  *           keyLabel="http://"
    108  *         /&gt;
    109  *       &lt;/case&gt;
    110  *     &lt;/switch&gt;
    111  *     ...
    112  *     &lt;Key keyStyle="shift-key" ... /&gt;
    113  * </pre>
    114  */
    115 
    116 public class KeyboardBuilder<KP extends KeyboardParams> {
    117     private static final String BUILDER_TAG = "Keyboard.Builder";
    118     private static final boolean DEBUG = false;
    119 
    120     // Keyboard XML Tags
    121     private static final String TAG_KEYBOARD = "Keyboard";
    122     private static final String TAG_ROW = "Row";
    123     private static final String TAG_KEY = "Key";
    124     private static final String TAG_SPACER = "Spacer";
    125     private static final String TAG_INCLUDE = "include";
    126     private static final String TAG_MERGE = "merge";
    127     private static final String TAG_SWITCH = "switch";
    128     private static final String TAG_CASE = "case";
    129     private static final String TAG_DEFAULT = "default";
    130     public static final String TAG_KEY_STYLE = "key-style";
    131 
    132     private static final int DEFAULT_KEYBOARD_COLUMNS = 10;
    133     private static final int DEFAULT_KEYBOARD_ROWS = 4;
    134 
    135     protected final KP mParams;
    136     protected final Context mContext;
    137     protected final Resources mResources;
    138 
    139     private int mCurrentY = 0;
    140     private KeyboardRow mCurrentRow = null;
    141     private boolean mLeftEdge;
    142     private boolean mTopEdge;
    143     private Key mRightEdgeKey = null;
    144 
    145     public KeyboardBuilder(final Context context, final KP params) {
    146         mContext = context;
    147         final Resources res = context.getResources();
    148         mResources = res;
    149 
    150         mParams = params;
    151 
    152         params.GRID_WIDTH = res.getInteger(R.integer.config_keyboard_grid_width);
    153         params.GRID_HEIGHT = res.getInteger(R.integer.config_keyboard_grid_height);
    154     }
    155 
    156     public void setAutoGenerate(final KeysCache keysCache) {
    157         mParams.mKeysCache = keysCache;
    158     }
    159 
    160     public KeyboardBuilder<KP> load(final int xmlId, final KeyboardId id) {
    161         mParams.mId = id;
    162         final XmlResourceParser parser = mResources.getXml(xmlId);
    163         try {
    164             parseKeyboard(parser);
    165         } catch (XmlPullParserException e) {
    166             Log.w(BUILDER_TAG, "keyboard XML parse error", e);
    167             throw new IllegalArgumentException(e.getMessage(), e);
    168         } catch (IOException e) {
    169             Log.w(BUILDER_TAG, "keyboard XML parse error", e);
    170             throw new RuntimeException(e.getMessage(), e);
    171         } finally {
    172             parser.close();
    173         }
    174         return this;
    175     }
    176 
    177     @UsedForTesting
    178     public void disableTouchPositionCorrectionDataForTest() {
    179         mParams.mTouchPositionCorrection.setEnabled(false);
    180     }
    181 
    182     public void setProximityCharsCorrectionEnabled(final boolean enabled) {
    183         mParams.mProximityCharsCorrectionEnabled = enabled;
    184     }
    185 
    186     public Keyboard build() {
    187         return new Keyboard(mParams);
    188     }
    189 
    190     private int mIndent;
    191     private static final String SPACES = "                                             ";
    192 
    193     private static String spaces(final int count) {
    194         return (count < SPACES.length()) ? SPACES.substring(0, count) : SPACES;
    195     }
    196 
    197     private void startTag(final String format, final Object ... args) {
    198         Log.d(BUILDER_TAG, String.format(spaces(++mIndent * 2) + format, args));
    199     }
    200 
    201     private void endTag(final String format, final Object ... args) {
    202         Log.d(BUILDER_TAG, String.format(spaces(mIndent-- * 2) + format, args));
    203     }
    204 
    205     private void startEndTag(final String format, final Object ... args) {
    206         Log.d(BUILDER_TAG, String.format(spaces(++mIndent * 2) + format, args));
    207         mIndent--;
    208     }
    209 
    210     private void parseKeyboard(final XmlPullParser parser)
    211             throws XmlPullParserException, IOException {
    212         if (DEBUG) startTag("<%s> %s", TAG_KEYBOARD, mParams.mId);
    213         while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
    214             final int event = parser.next();
    215             if (event == XmlPullParser.START_TAG) {
    216                 final String tag = parser.getName();
    217                 if (TAG_KEYBOARD.equals(tag)) {
    218                     parseKeyboardAttributes(parser);
    219                     startKeyboard();
    220                     parseKeyboardContent(parser, false);
    221                     break;
    222                 } else {
    223                     throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD);
    224                 }
    225             }
    226         }
    227     }
    228 
    229     private void parseKeyboardAttributes(final XmlPullParser parser) {
    230         final TypedArray keyboardAttr = mContext.obtainStyledAttributes(
    231                 Xml.asAttributeSet(parser), R.styleable.Keyboard, R.attr.keyboardStyle,
    232                 R.style.Keyboard);
    233         final TypedArray keyAttr = mResources.obtainAttributes(Xml.asAttributeSet(parser),
    234                 R.styleable.Keyboard_Key);
    235         try {
    236             final KeyboardParams params = mParams;
    237             final int height = params.mId.mHeight;
    238             final int width = params.mId.mWidth;
    239             params.mOccupiedHeight = height;
    240             params.mOccupiedWidth = width;
    241             params.mTopPadding = (int)keyboardAttr.getFraction(
    242                     R.styleable.Keyboard_keyboardTopPadding, height, height, 0);
    243             params.mBottomPadding = (int)keyboardAttr.getFraction(
    244                     R.styleable.Keyboard_keyboardBottomPadding, height, height, 0);
    245             params.mLeftPadding = (int)keyboardAttr.getFraction(
    246                     R.styleable.Keyboard_keyboardLeftPadding, width, width, 0);
    247             params.mRightPadding = (int)keyboardAttr.getFraction(
    248                     R.styleable.Keyboard_keyboardRightPadding, width, width, 0);
    249 
    250             final int baseWidth =
    251                     params.mOccupiedWidth - params.mLeftPadding - params.mRightPadding;
    252             params.mBaseWidth = baseWidth;
    253             params.mDefaultKeyWidth = (int)keyAttr.getFraction(R.styleable.Keyboard_Key_keyWidth,
    254                     baseWidth, baseWidth, baseWidth / DEFAULT_KEYBOARD_COLUMNS);
    255             params.mHorizontalGap = (int)keyboardAttr.getFraction(
    256                     R.styleable.Keyboard_horizontalGap, baseWidth, baseWidth, 0);
    257             // TODO: Fix keyboard geometry calculation clearer. Historically vertical gap between
    258             // rows are determined based on the entire keyboard height including top and bottom
    259             // paddings.
    260             params.mVerticalGap = (int)keyboardAttr.getFraction(
    261                     R.styleable.Keyboard_verticalGap, height, height, 0);
    262             final int baseHeight = params.mOccupiedHeight - params.mTopPadding
    263                     - params.mBottomPadding + params.mVerticalGap;
    264             params.mBaseHeight = baseHeight;
    265             params.mDefaultRowHeight = (int)ResourceUtils.getDimensionOrFraction(keyboardAttr,
    266                     R.styleable.Keyboard_rowHeight, baseHeight, baseHeight / DEFAULT_KEYBOARD_ROWS);
    267 
    268             params.mKeyVisualAttributes = KeyVisualAttributes.newInstance(keyAttr);
    269 
    270             params.mMoreKeysTemplate = keyboardAttr.getResourceId(
    271                     R.styleable.Keyboard_moreKeysTemplate, 0);
    272             params.mMaxMoreKeysKeyboardColumn = keyAttr.getInt(
    273                     R.styleable.Keyboard_Key_maxMoreKeysColumn, 5);
    274 
    275             params.mThemeId = keyboardAttr.getInt(R.styleable.Keyboard_themeId, 0);
    276             params.mIconsSet.loadIcons(keyboardAttr);
    277             final String language = params.mId.mLocale.getLanguage();
    278             params.mCodesSet.setLanguage(language);
    279             params.mTextsSet.setLanguage(language);
    280             final RunInLocale<Void> job = new RunInLocale<Void>() {
    281                 @Override
    282                 protected Void job(Resources res) {
    283                     params.mTextsSet.loadStringResources(mContext);
    284                     return null;
    285                 }
    286             };
    287             // Null means the current system locale.
    288             final Locale locale = SubtypeLocale.isNoLanguage(params.mId.mSubtype)
    289                     ? null : params.mId.mLocale;
    290             job.runInLocale(mResources, locale);
    291 
    292             final int resourceId = keyboardAttr.getResourceId(
    293                     R.styleable.Keyboard_touchPositionCorrectionData, 0);
    294             if (resourceId != 0) {
    295                 final String[] data = mResources.getStringArray(resourceId);
    296                 params.mTouchPositionCorrection.load(data);
    297             }
    298         } finally {
    299             keyAttr.recycle();
    300             keyboardAttr.recycle();
    301         }
    302     }
    303 
    304     private void parseKeyboardContent(final XmlPullParser parser, final boolean skip)
    305             throws XmlPullParserException, IOException {
    306         while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
    307             final int event = parser.next();
    308             if (event == XmlPullParser.START_TAG) {
    309                 final String tag = parser.getName();
    310                 if (TAG_ROW.equals(tag)) {
    311                     final KeyboardRow row = parseRowAttributes(parser);
    312                     if (DEBUG) startTag("<%s>%s", TAG_ROW, skip ? " skipped" : "");
    313                     if (!skip) {
    314                         startRow(row);
    315                     }
    316                     parseRowContent(parser, row, skip);
    317                 } else if (TAG_INCLUDE.equals(tag)) {
    318                     parseIncludeKeyboardContent(parser, skip);
    319                 } else if (TAG_SWITCH.equals(tag)) {
    320                     parseSwitchKeyboardContent(parser, skip);
    321                 } else if (TAG_KEY_STYLE.equals(tag)) {
    322                     parseKeyStyle(parser, skip);
    323                 } else {
    324                     throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_ROW);
    325                 }
    326             } else if (event == XmlPullParser.END_TAG) {
    327                 final String tag = parser.getName();
    328                 if (DEBUG) endTag("</%s>", tag);
    329                 if (TAG_KEYBOARD.equals(tag)) {
    330                     endKeyboard();
    331                     break;
    332                 } else if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag)
    333                         || TAG_MERGE.equals(tag)) {
    334                     break;
    335                 } else {
    336                     throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_ROW);
    337                 }
    338             }
    339         }
    340     }
    341 
    342     private KeyboardRow parseRowAttributes(final XmlPullParser parser)
    343             throws XmlPullParserException {
    344         final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
    345                 R.styleable.Keyboard);
    346         try {
    347             if (a.hasValue(R.styleable.Keyboard_horizontalGap)) {
    348                 throw new XmlParseUtils.IllegalAttribute(parser, TAG_ROW, "horizontalGap");
    349             }
    350             if (a.hasValue(R.styleable.Keyboard_verticalGap)) {
    351                 throw new XmlParseUtils.IllegalAttribute(parser, TAG_ROW, "verticalGap");
    352             }
    353             return new KeyboardRow(mResources, mParams, parser, mCurrentY);
    354         } finally {
    355             a.recycle();
    356         }
    357     }
    358 
    359     private void parseRowContent(final XmlPullParser parser, final KeyboardRow row,
    360             final boolean skip) throws XmlPullParserException, IOException {
    361         while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
    362             final int event = parser.next();
    363             if (event == XmlPullParser.START_TAG) {
    364                 final String tag = parser.getName();
    365                 if (TAG_KEY.equals(tag)) {
    366                     parseKey(parser, row, skip);
    367                 } else if (TAG_SPACER.equals(tag)) {
    368                     parseSpacer(parser, row, skip);
    369                 } else if (TAG_INCLUDE.equals(tag)) {
    370                     parseIncludeRowContent(parser, row, skip);
    371                 } else if (TAG_SWITCH.equals(tag)) {
    372                     parseSwitchRowContent(parser, row, skip);
    373                 } else if (TAG_KEY_STYLE.equals(tag)) {
    374                     parseKeyStyle(parser, skip);
    375                 } else {
    376                     throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_ROW);
    377                 }
    378             } else if (event == XmlPullParser.END_TAG) {
    379                 final String tag = parser.getName();
    380                 if (DEBUG) endTag("</%s>", tag);
    381                 if (TAG_ROW.equals(tag)) {
    382                     if (!skip) {
    383                         endRow(row);
    384                     }
    385                     break;
    386                 } else if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag)
    387                         || TAG_MERGE.equals(tag)) {
    388                     break;
    389                 } else {
    390                     throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_ROW);
    391                 }
    392             }
    393         }
    394     }
    395 
    396     private void parseKey(final XmlPullParser parser, final KeyboardRow row, final boolean skip)
    397             throws XmlPullParserException, IOException {
    398         if (skip) {
    399             XmlParseUtils.checkEndTag(TAG_KEY, parser);
    400             if (DEBUG) {
    401                 startEndTag("<%s /> skipped", TAG_KEY);
    402             }
    403         } else {
    404             final Key key = new Key(mResources, mParams, row, parser);
    405             if (DEBUG) {
    406                 startEndTag("<%s%s %s moreKeys=%s />", TAG_KEY,
    407                         (key.isEnabled() ? "" : " disabled"), key,
    408                         Arrays.toString(key.mMoreKeys));
    409             }
    410             XmlParseUtils.checkEndTag(TAG_KEY, parser);
    411             endKey(key);
    412         }
    413     }
    414 
    415     private void parseSpacer(final XmlPullParser parser, final KeyboardRow row, final boolean skip)
    416             throws XmlPullParserException, IOException {
    417         if (skip) {
    418             XmlParseUtils.checkEndTag(TAG_SPACER, parser);
    419             if (DEBUG) startEndTag("<%s /> skipped", TAG_SPACER);
    420         } else {
    421             final Key.Spacer spacer = new Key.Spacer(mResources, mParams, row, parser);
    422             if (DEBUG) startEndTag("<%s />", TAG_SPACER);
    423             XmlParseUtils.checkEndTag(TAG_SPACER, parser);
    424             endKey(spacer);
    425         }
    426     }
    427 
    428     private void parseIncludeKeyboardContent(final XmlPullParser parser, final boolean skip)
    429             throws XmlPullParserException, IOException {
    430         parseIncludeInternal(parser, null, skip);
    431     }
    432 
    433     private void parseIncludeRowContent(final XmlPullParser parser, final KeyboardRow row,
    434             final boolean skip) throws XmlPullParserException, IOException {
    435         parseIncludeInternal(parser, row, skip);
    436     }
    437 
    438     private void parseIncludeInternal(final XmlPullParser parser, final KeyboardRow row,
    439             final boolean skip) throws XmlPullParserException, IOException {
    440         if (skip) {
    441             XmlParseUtils.checkEndTag(TAG_INCLUDE, parser);
    442             if (DEBUG) startEndTag("</%s> skipped", TAG_INCLUDE);
    443         } else {
    444             final AttributeSet attr = Xml.asAttributeSet(parser);
    445             final TypedArray keyboardAttr = mResources.obtainAttributes(attr,
    446                     R.styleable.Keyboard_Include);
    447             final TypedArray keyAttr = mResources.obtainAttributes(attr,
    448                     R.styleable.Keyboard_Key);
    449             int keyboardLayout = 0;
    450             float savedDefaultKeyWidth = 0;
    451             int savedDefaultKeyLabelFlags = 0;
    452             int savedDefaultBackgroundType = Key.BACKGROUND_TYPE_NORMAL;
    453             try {
    454                 XmlParseUtils.checkAttributeExists(keyboardAttr,
    455                         R.styleable.Keyboard_Include_keyboardLayout, "keyboardLayout",
    456                         TAG_INCLUDE, parser);
    457                 keyboardLayout = keyboardAttr.getResourceId(
    458                         R.styleable.Keyboard_Include_keyboardLayout, 0);
    459                 if (row != null) {
    460                     if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyXPos)) {
    461                         // Override current x coordinate.
    462                         row.setXPos(row.getKeyX(keyAttr));
    463                     }
    464                     // TODO: Remove this if-clause and do the same as backgroundType below.
    465                     savedDefaultKeyWidth = row.getDefaultKeyWidth();
    466                     if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyWidth)) {
    467                         // Override default key width.
    468                         row.setDefaultKeyWidth(row.getKeyWidth(keyAttr));
    469                     }
    470                     savedDefaultKeyLabelFlags = row.getDefaultKeyLabelFlags();
    471                     // Bitwise-or default keyLabelFlag if exists.
    472                     row.setDefaultKeyLabelFlags(keyAttr.getInt(
    473                             R.styleable.Keyboard_Key_keyLabelFlags, 0)
    474                             | savedDefaultKeyLabelFlags);
    475                     savedDefaultBackgroundType = row.getDefaultBackgroundType();
    476                     // Override default backgroundType if exists.
    477                     row.setDefaultBackgroundType(keyAttr.getInt(
    478                             R.styleable.Keyboard_Key_backgroundType,
    479                             savedDefaultBackgroundType));
    480                 }
    481             } finally {
    482                 keyboardAttr.recycle();
    483                 keyAttr.recycle();
    484             }
    485 
    486             XmlParseUtils.checkEndTag(TAG_INCLUDE, parser);
    487             if (DEBUG) {
    488                 startEndTag("<%s keyboardLayout=%s />",TAG_INCLUDE,
    489                         mResources.getResourceEntryName(keyboardLayout));
    490             }
    491             final XmlResourceParser parserForInclude = mResources.getXml(keyboardLayout);
    492             try {
    493                 parseMerge(parserForInclude, row, skip);
    494             } finally {
    495                 if (row != null) {
    496                     // Restore default keyWidth, keyLabelFlags, and backgroundType.
    497                     row.setDefaultKeyWidth(savedDefaultKeyWidth);
    498                     row.setDefaultKeyLabelFlags(savedDefaultKeyLabelFlags);
    499                     row.setDefaultBackgroundType(savedDefaultBackgroundType);
    500                 }
    501                 parserForInclude.close();
    502             }
    503         }
    504     }
    505 
    506     private void parseMerge(final XmlPullParser parser, final KeyboardRow row, final boolean skip)
    507             throws XmlPullParserException, IOException {
    508         if (DEBUG) startTag("<%s>", TAG_MERGE);
    509         while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
    510             final int event = parser.next();
    511             if (event == XmlPullParser.START_TAG) {
    512                 final String tag = parser.getName();
    513                 if (TAG_MERGE.equals(tag)) {
    514                     if (row == null) {
    515                         parseKeyboardContent(parser, skip);
    516                     } else {
    517                         parseRowContent(parser, row, skip);
    518                     }
    519                     break;
    520                 } else {
    521                     throw new XmlParseUtils.ParseException(
    522                             "Included keyboard layout must have <merge> root element", parser);
    523                 }
    524             }
    525         }
    526     }
    527 
    528     private void parseSwitchKeyboardContent(final XmlPullParser parser, final boolean skip)
    529             throws XmlPullParserException, IOException {
    530         parseSwitchInternal(parser, null, skip);
    531     }
    532 
    533     private void parseSwitchRowContent(final XmlPullParser parser, final KeyboardRow row,
    534             final boolean skip) throws XmlPullParserException, IOException {
    535         parseSwitchInternal(parser, row, skip);
    536     }
    537 
    538     private void parseSwitchInternal(final XmlPullParser parser, final KeyboardRow row,
    539             final boolean skip) throws XmlPullParserException, IOException {
    540         if (DEBUG) startTag("<%s> %s", TAG_SWITCH, mParams.mId);
    541         boolean selected = false;
    542         while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
    543             final int event = parser.next();
    544             if (event == XmlPullParser.START_TAG) {
    545                 final String tag = parser.getName();
    546                 if (TAG_CASE.equals(tag)) {
    547                     selected |= parseCase(parser, row, selected ? true : skip);
    548                 } else if (TAG_DEFAULT.equals(tag)) {
    549                     selected |= parseDefault(parser, row, selected ? true : skip);
    550                 } else {
    551                     throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_SWITCH);
    552                 }
    553             } else if (event == XmlPullParser.END_TAG) {
    554                 final String tag = parser.getName();
    555                 if (TAG_SWITCH.equals(tag)) {
    556                     if (DEBUG) endTag("</%s>", TAG_SWITCH);
    557                     break;
    558                 } else {
    559                     throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_SWITCH);
    560                 }
    561             }
    562         }
    563     }
    564 
    565     private boolean parseCase(final XmlPullParser parser, final KeyboardRow row, final boolean skip)
    566             throws XmlPullParserException, IOException {
    567         final boolean selected = parseCaseCondition(parser);
    568         if (row == null) {
    569             // Processing Rows.
    570             parseKeyboardContent(parser, selected ? skip : true);
    571         } else {
    572             // Processing Keys.
    573             parseRowContent(parser, row, selected ? skip : true);
    574         }
    575         return selected;
    576     }
    577 
    578     private boolean parseCaseCondition(final XmlPullParser parser) {
    579         final KeyboardId id = mParams.mId;
    580         if (id == null) {
    581             return true;
    582         }
    583         final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
    584                 R.styleable.Keyboard_Case);
    585         try {
    586             final boolean keyboardLayoutSetElementMatched = matchTypedValue(a,
    587                     R.styleable.Keyboard_Case_keyboardLayoutSetElement, id.mElementId,
    588                     KeyboardId.elementIdToName(id.mElementId));
    589             final boolean modeMatched = matchTypedValue(a,
    590                     R.styleable.Keyboard_Case_mode, id.mMode, KeyboardId.modeName(id.mMode));
    591             final boolean navigateNextMatched = matchBoolean(a,
    592                     R.styleable.Keyboard_Case_navigateNext, id.navigateNext());
    593             final boolean navigatePreviousMatched = matchBoolean(a,
    594                     R.styleable.Keyboard_Case_navigatePrevious, id.navigatePrevious());
    595             final boolean passwordInputMatched = matchBoolean(a,
    596                     R.styleable.Keyboard_Case_passwordInput, id.passwordInput());
    597             final boolean clobberSettingsKeyMatched = matchBoolean(a,
    598                     R.styleable.Keyboard_Case_clobberSettingsKey, id.mClobberSettingsKey);
    599             final boolean shortcutKeyEnabledMatched = matchBoolean(a,
    600                     R.styleable.Keyboard_Case_shortcutKeyEnabled, id.mShortcutKeyEnabled);
    601             final boolean shortcutKeyOnSymbolsMatched = matchBoolean(a,
    602                     R.styleable.Keyboard_Case_shortcutKeyOnSymbols, id.mShortcutKeyOnSymbols);
    603             final boolean hasShortcutKeyMatched = matchBoolean(a,
    604                     R.styleable.Keyboard_Case_hasShortcutKey, id.mHasShortcutKey);
    605             final boolean languageSwitchKeyEnabledMatched = matchBoolean(a,
    606                     R.styleable.Keyboard_Case_languageSwitchKeyEnabled,
    607                     id.mLanguageSwitchKeyEnabled);
    608             final boolean isMultiLineMatched = matchBoolean(a,
    609                     R.styleable.Keyboard_Case_isMultiLine, id.isMultiLine());
    610             final boolean imeActionMatched = matchInteger(a,
    611                     R.styleable.Keyboard_Case_imeAction, id.imeAction());
    612             final boolean localeCodeMatched = matchString(a,
    613                     R.styleable.Keyboard_Case_localeCode, id.mLocale.toString());
    614             final boolean languageCodeMatched = matchString(a,
    615                     R.styleable.Keyboard_Case_languageCode, id.mLocale.getLanguage());
    616             final boolean countryCodeMatched = matchString(a,
    617                     R.styleable.Keyboard_Case_countryCode, id.mLocale.getCountry());
    618             final boolean selected = keyboardLayoutSetElementMatched && modeMatched
    619                     && navigateNextMatched && navigatePreviousMatched && passwordInputMatched
    620                     && clobberSettingsKeyMatched && shortcutKeyEnabledMatched
    621                     && shortcutKeyOnSymbolsMatched && hasShortcutKeyMatched
    622                     && languageSwitchKeyEnabledMatched && isMultiLineMatched && imeActionMatched
    623                     && localeCodeMatched && languageCodeMatched && countryCodeMatched;
    624 
    625             if (DEBUG) {
    626                 startTag("<%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s>%s", TAG_CASE,
    627                         textAttr(a.getString(
    628                                 R.styleable.Keyboard_Case_keyboardLayoutSetElement),
    629                                 "keyboardLayoutSetElement"),
    630                         textAttr(a.getString(R.styleable.Keyboard_Case_mode), "mode"),
    631                         textAttr(a.getString(R.styleable.Keyboard_Case_imeAction),
    632                                 "imeAction"),
    633                         booleanAttr(a, R.styleable.Keyboard_Case_navigateNext,
    634                                 "navigateNext"),
    635                         booleanAttr(a, R.styleable.Keyboard_Case_navigatePrevious,
    636                                 "navigatePrevious"),
    637                         booleanAttr(a, R.styleable.Keyboard_Case_clobberSettingsKey,
    638                                 "clobberSettingsKey"),
    639                         booleanAttr(a, R.styleable.Keyboard_Case_passwordInput,
    640                                 "passwordInput"),
    641                         booleanAttr(a, R.styleable.Keyboard_Case_shortcutKeyEnabled,
    642                                 "shortcutKeyEnabled"),
    643                         booleanAttr(a, R.styleable.Keyboard_Case_shortcutKeyOnSymbols,
    644                                 "shortcutKeyOnSymbols"),
    645                         booleanAttr(a, R.styleable.Keyboard_Case_hasShortcutKey,
    646                                 "hasShortcutKey"),
    647                         booleanAttr(a, R.styleable.Keyboard_Case_languageSwitchKeyEnabled,
    648                                 "languageSwitchKeyEnabled"),
    649                         booleanAttr(a, R.styleable.Keyboard_Case_isMultiLine,
    650                                 "isMultiLine"),
    651                         textAttr(a.getString(R.styleable.Keyboard_Case_localeCode),
    652                                 "localeCode"),
    653                         textAttr(a.getString(R.styleable.Keyboard_Case_languageCode),
    654                                 "languageCode"),
    655                         textAttr(a.getString(R.styleable.Keyboard_Case_countryCode),
    656                                 "countryCode"),
    657                         selected ? "" : " skipped");
    658             }
    659 
    660             return selected;
    661         } finally {
    662             a.recycle();
    663         }
    664     }
    665 
    666     private static boolean matchInteger(final TypedArray a, final int index, final int value) {
    667         // If <case> does not have "index" attribute, that means this <case> is wild-card for
    668         // the attribute.
    669         return !a.hasValue(index) || a.getInt(index, 0) == value;
    670     }
    671 
    672     private static boolean matchBoolean(final TypedArray a, final int index, final boolean value) {
    673         // If <case> does not have "index" attribute, that means this <case> is wild-card for
    674         // the attribute.
    675         return !a.hasValue(index) || a.getBoolean(index, false) == value;
    676     }
    677 
    678     private static boolean matchString(final TypedArray a, final int index, final String value) {
    679         // If <case> does not have "index" attribute, that means this <case> is wild-card for
    680         // the attribute.
    681         return !a.hasValue(index)
    682                 || StringUtils.containsInArray(value, a.getString(index).split("\\|"));
    683     }
    684 
    685     private static boolean matchTypedValue(final TypedArray a, final int index, final int intValue,
    686             final String strValue) {
    687         // If <case> does not have "index" attribute, that means this <case> is wild-card for
    688         // the attribute.
    689         final TypedValue v = a.peekValue(index);
    690         if (v == null) {
    691             return true;
    692         }
    693         if (ResourceUtils.isIntegerValue(v)) {
    694             return intValue == a.getInt(index, 0);
    695         } else if (ResourceUtils.isStringValue(v)) {
    696             return StringUtils.containsInArray(strValue, a.getString(index).split("\\|"));
    697         }
    698         return false;
    699     }
    700 
    701     private boolean parseDefault(final XmlPullParser parser, final KeyboardRow row,
    702             final boolean skip) throws XmlPullParserException, IOException {
    703         if (DEBUG) startTag("<%s>", TAG_DEFAULT);
    704         if (row == null) {
    705             parseKeyboardContent(parser, skip);
    706         } else {
    707             parseRowContent(parser, row, skip);
    708         }
    709         return true;
    710     }
    711 
    712     private void parseKeyStyle(final XmlPullParser parser, final boolean skip)
    713             throws XmlPullParserException, IOException {
    714         TypedArray keyStyleAttr = mResources.obtainAttributes(Xml.asAttributeSet(parser),
    715                 R.styleable.Keyboard_KeyStyle);
    716         TypedArray keyAttrs = mResources.obtainAttributes(Xml.asAttributeSet(parser),
    717                 R.styleable.Keyboard_Key);
    718         try {
    719             if (!keyStyleAttr.hasValue(R.styleable.Keyboard_KeyStyle_styleName)) {
    720                 throw new XmlParseUtils.ParseException("<" + TAG_KEY_STYLE
    721                         + "/> needs styleName attribute", parser);
    722             }
    723             if (DEBUG) {
    724                 startEndTag("<%s styleName=%s />%s", TAG_KEY_STYLE,
    725                         keyStyleAttr.getString(R.styleable.Keyboard_KeyStyle_styleName),
    726                         skip ? " skipped" : "");
    727             }
    728             if (!skip) {
    729                 mParams.mKeyStyles.parseKeyStyleAttributes(keyStyleAttr, keyAttrs, parser);
    730             }
    731         } finally {
    732             keyStyleAttr.recycle();
    733             keyAttrs.recycle();
    734         }
    735         XmlParseUtils.checkEndTag(TAG_KEY_STYLE, parser);
    736     }
    737 
    738     private void startKeyboard() {
    739         mCurrentY += mParams.mTopPadding;
    740         mTopEdge = true;
    741     }
    742 
    743     private void startRow(final KeyboardRow row) {
    744         addEdgeSpace(mParams.mLeftPadding, row);
    745         mCurrentRow = row;
    746         mLeftEdge = true;
    747         mRightEdgeKey = null;
    748     }
    749 
    750     private void endRow(final KeyboardRow row) {
    751         if (mCurrentRow == null) {
    752             throw new RuntimeException("orphan end row tag");
    753         }
    754         if (mRightEdgeKey != null) {
    755             mRightEdgeKey.markAsRightEdge(mParams);
    756             mRightEdgeKey = null;
    757         }
    758         addEdgeSpace(mParams.mRightPadding, row);
    759         mCurrentY += row.mRowHeight;
    760         mCurrentRow = null;
    761         mTopEdge = false;
    762     }
    763 
    764     private void endKey(final Key key) {
    765         mParams.onAddKey(key);
    766         if (mLeftEdge) {
    767             key.markAsLeftEdge(mParams);
    768             mLeftEdge = false;
    769         }
    770         if (mTopEdge) {
    771             key.markAsTopEdge(mParams);
    772         }
    773         mRightEdgeKey = key;
    774     }
    775 
    776     private void endKeyboard() {
    777         // nothing to do here.
    778     }
    779 
    780     private void addEdgeSpace(final float width, final KeyboardRow row) {
    781         row.advanceXPos(width);
    782         mLeftEdge = false;
    783         mRightEdgeKey = null;
    784     }
    785 
    786     private static String textAttr(final String value, final String name) {
    787         return value != null ? String.format(" %s=%s", name, value) : "";
    788     }
    789 
    790     private static String booleanAttr(final TypedArray a, final int index, final String name) {
    791         return a.hasValue(index)
    792                 ? String.format(" %s=%s", name, a.getBoolean(index, false)) : "";
    793     }
    794 }
    795