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