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