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