Home | History | Annotate | Download | only in internal
      1 /*
      2  * Copyright (C) 2010 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.DisplayMetrics;
     24 import android.util.Log;
     25 import android.util.TypedValue;
     26 import android.util.Xml;
     27 import android.view.InflateException;
     28 
     29 import com.android.inputmethod.compat.EditorInfoCompatUtils;
     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.LatinImeLogger;
     34 import com.android.inputmethod.latin.R;
     35 
     36 import org.xmlpull.v1.XmlPullParser;
     37 import org.xmlpull.v1.XmlPullParserException;
     38 
     39 import java.io.IOException;
     40 import java.util.Arrays;
     41 
     42 /**
     43  * Keyboard Building helper.
     44  *
     45  * This class parses Keyboard XML file and eventually build a Keyboard.
     46  * The Keyboard XML file looks like:
     47  * <pre>
     48  *   &gt;!-- xml/keyboard.xml --&lt;
     49  *   &gt;Keyboard keyboard_attributes*&lt;
     50  *     &gt;!-- Keyboard Content --&lt;
     51  *     &gt;Row row_attributes*&lt;
     52  *       &gt;!-- Row Content --&lt;
     53  *       &gt;Key key_attributes* /&lt;
     54  *       &gt;Spacer horizontalGap="0.2in" /&lt;
     55  *       &gt;include keyboardLayout="@xml/other_keys"&lt;
     56  *       ...
     57  *     &gt;/Row&lt;
     58  *     &gt;include keyboardLayout="@xml/other_rows"&lt;
     59  *     ...
     60  *   &gt;/Keyboard&lt;
     61  * </pre>
     62  * The XML file which is included in other file must have &gt;merge&lt; as root element, such as:
     63  * <pre>
     64  *   &gt;!-- xml/other_keys.xml --&lt;
     65  *   &gt;merge&lt;
     66  *     &gt;Key key_attributes* /&lt;
     67  *     ...
     68  *   &gt;/merge&lt;
     69  * </pre>
     70  * and
     71  * <pre>
     72  *   &gt;!-- xml/other_rows.xml --&lt;
     73  *   &gt;merge&lt;
     74  *     &gt;Row row_attributes*&lt;
     75  *       &gt;Key key_attributes* /&lt;
     76  *     &gt;/Row&lt;
     77  *     ...
     78  *   &gt;/merge&lt;
     79  * </pre>
     80  * You can also use switch-case-default tags to select Rows and Keys.
     81  * <pre>
     82  *   &gt;switch&lt;
     83  *     &gt;case case_attribute*&lt;
     84  *       &gt;!-- Any valid tags at switch position --&lt;
     85  *     &gt;/case&lt;
     86  *     ...
     87  *     &gt;default&lt;
     88  *       &gt;!-- Any valid tags at switch position --&lt;
     89  *     &gt;/default&lt;
     90  *   &gt;/switch&lt;
     91  * </pre>
     92  * You can declare Key style and specify styles within Key tags.
     93  * <pre>
     94  *     &gt;switch&lt;
     95  *       &gt;case mode="email"&lt;
     96  *         &gt;key-style styleName="f1-key" parentStyle="modifier-key"
     97  *           keyLabel=".com"
     98  *         /&lt;
     99  *       &gt;/case&lt;
    100  *       &gt;case mode="url"&lt;
    101  *         &gt;key-style styleName="f1-key" parentStyle="modifier-key"
    102  *           keyLabel="http://"
    103  *         /&lt;
    104  *       &gt;/case&lt;
    105  *     &gt;/switch&lt;
    106  *     ...
    107  *     &gt;Key keyStyle="shift-key" ... /&lt;
    108  * </pre>
    109  */
    110 
    111 public class KeyboardBuilder<KP extends KeyboardParams> {
    112     private static final String TAG = KeyboardBuilder.class.getSimpleName();
    113     private static final boolean DEBUG = false;
    114 
    115     // Keyboard XML Tags
    116     private static final String TAG_KEYBOARD = "Keyboard";
    117     private static final String TAG_ROW = "Row";
    118     private static final String TAG_KEY = "Key";
    119     private static final String TAG_SPACER = "Spacer";
    120     private static final String TAG_INCLUDE = "include";
    121     private static final String TAG_MERGE = "merge";
    122     private static final String TAG_SWITCH = "switch";
    123     private static final String TAG_CASE = "case";
    124     private static final String TAG_DEFAULT = "default";
    125     public static final String TAG_KEY_STYLE = "key-style";
    126 
    127     private static final int DEFAULT_KEYBOARD_COLUMNS = 10;
    128     private static final int DEFAULT_KEYBOARD_ROWS = 4;
    129 
    130     protected final KP mParams;
    131     protected final Context mContext;
    132     protected final Resources mResources;
    133     private final DisplayMetrics mDisplayMetrics;
    134 
    135     private int mCurrentY = 0;
    136     private Row mCurrentRow = null;
    137     private boolean mLeftEdge;
    138     private boolean mTopEdge;
    139     private Key mRightEdgeKey = null;
    140     private final KeyStyles mKeyStyles = new KeyStyles();
    141 
    142     /**
    143      * Container for keys in the keyboard. All keys in a row are at the same Y-coordinate.
    144      * Some of the key size defaults can be overridden per row from what the {@link Keyboard}
    145      * defines.
    146      */
    147     public static class Row {
    148         // keyWidth enum constants
    149         private static final int KEYWIDTH_NOT_ENUM = 0;
    150         private static final int KEYWIDTH_FILL_RIGHT = -1;
    151         private static final int KEYWIDTH_FILL_BOTH = -2;
    152 
    153         private final KeyboardParams mParams;
    154         /** Default width of a key in this row. */
    155         public final float mDefaultKeyWidth;
    156         /** Default height of a key in this row. */
    157         public final int mRowHeight;
    158 
    159         private final int mCurrentY;
    160         // Will be updated by {@link Key}'s constructor.
    161         private float mCurrentX;
    162 
    163         public Row(Resources res, KeyboardParams params, XmlPullParser parser, int y) {
    164             mParams = params;
    165             TypedArray keyboardAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
    166                     R.styleable.Keyboard);
    167             mRowHeight = (int)KeyboardBuilder.getDimensionOrFraction(keyboardAttr,
    168                     R.styleable.Keyboard_rowHeight, params.mBaseHeight, params.mDefaultRowHeight);
    169             keyboardAttr.recycle();
    170             TypedArray keyAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
    171                     R.styleable.Keyboard_Key);
    172             mDefaultKeyWidth = KeyboardBuilder.getDimensionOrFraction(keyAttr,
    173                     R.styleable.Keyboard_Key_keyWidth, params.mBaseWidth, params.mDefaultKeyWidth);
    174             keyAttr.recycle();
    175 
    176             mCurrentY = y;
    177             mCurrentX = 0.0f;
    178         }
    179 
    180         public void setXPos(float keyXPos) {
    181             mCurrentX = keyXPos;
    182         }
    183 
    184         public void advanceXPos(float width) {
    185             mCurrentX += width;
    186         }
    187 
    188         public int getKeyY() {
    189             return mCurrentY;
    190         }
    191 
    192         public float getKeyX(TypedArray keyAttr) {
    193             final int widthType = KeyboardBuilder.getEnumValue(keyAttr,
    194                     R.styleable.Keyboard_Key_keyWidth, KEYWIDTH_NOT_ENUM);
    195             if (widthType == KEYWIDTH_FILL_BOTH) {
    196                 // If keyWidth is fillBoth, the key width should start right after the nearest key
    197                 // on the left hand side.
    198                 return mCurrentX;
    199             }
    200 
    201             final int keyboardRightEdge = mParams.mOccupiedWidth - mParams.mHorizontalEdgesPadding;
    202             if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyXPos)) {
    203                 final float keyXPos = KeyboardBuilder.getDimensionOrFraction(keyAttr,
    204                         R.styleable.Keyboard_Key_keyXPos, mParams.mBaseWidth, 0);
    205                 if (keyXPos < 0) {
    206                     // If keyXPos is negative, the actual x-coordinate will be
    207                     // keyboardWidth + keyXPos.
    208                     // keyXPos shouldn't be less than mCurrentX because drawable area for this key
    209                     // starts at mCurrentX. Or, this key will overlaps the adjacent key on its left
    210                     // hand side.
    211                     return Math.max(keyXPos + keyboardRightEdge, mCurrentX);
    212                 } else {
    213                     return keyXPos + mParams.mHorizontalEdgesPadding;
    214                 }
    215             }
    216             return mCurrentX;
    217         }
    218 
    219         public float getKeyWidth(TypedArray keyAttr, float keyXPos) {
    220             final int widthType = KeyboardBuilder.getEnumValue(keyAttr,
    221                     R.styleable.Keyboard_Key_keyWidth, KEYWIDTH_NOT_ENUM);
    222             switch (widthType) {
    223             case KEYWIDTH_FILL_RIGHT:
    224             case KEYWIDTH_FILL_BOTH:
    225                 final int keyboardRightEdge =
    226                         mParams.mOccupiedWidth - mParams.mHorizontalEdgesPadding;
    227                 // If keyWidth is fillRight, the actual key width will be determined to fill out the
    228                 // area up to the right edge of the keyboard.
    229                 // If keyWidth is fillBoth, the actual key width will be determined to fill out the
    230                 // area between the nearest key on the left hand side and the right edge of the
    231                 // keyboard.
    232                 return keyboardRightEdge - keyXPos;
    233             default: // KEYWIDTH_NOT_ENUM
    234                 return KeyboardBuilder.getDimensionOrFraction(keyAttr,
    235                         R.styleable.Keyboard_Key_keyWidth, mParams.mBaseWidth, mDefaultKeyWidth);
    236             }
    237         }
    238     }
    239 
    240     public KeyboardBuilder(Context context, KP params) {
    241         mContext = context;
    242         final Resources res = context.getResources();
    243         mResources = res;
    244         mDisplayMetrics = res.getDisplayMetrics();
    245 
    246         mParams = params;
    247 
    248         setTouchPositionCorrectionData(context, params);
    249 
    250         params.GRID_WIDTH = res.getInteger(R.integer.config_keyboard_grid_width);
    251         params.GRID_HEIGHT = res.getInteger(R.integer.config_keyboard_grid_height);
    252     }
    253 
    254     private static void setTouchPositionCorrectionData(Context context, KeyboardParams params) {
    255         final TypedArray a = context.obtainStyledAttributes(
    256                 null, R.styleable.Keyboard, R.attr.keyboardStyle, 0);
    257         params.mThemeId = a.getInt(R.styleable.Keyboard_themeId, 0);
    258         final int resourceId = a.getResourceId(R.styleable.Keyboard_touchPositionCorrectionData, 0);
    259         a.recycle();
    260         if (resourceId == 0) {
    261             if (LatinImeLogger.sDBG)
    262                 throw new RuntimeException("touchPositionCorrectionData is not defined");
    263             return;
    264         }
    265 
    266         final String[] data = context.getResources().getStringArray(resourceId);
    267         params.mTouchPositionCorrection.load(data);
    268     }
    269 
    270     public KeyboardBuilder<KP> load(KeyboardId id) {
    271         mParams.mId = id;
    272         final XmlResourceParser parser = mResources.getXml(id.getXmlId());
    273         try {
    274             parseKeyboard(parser);
    275         } catch (XmlPullParserException e) {
    276             Log.w(TAG, "keyboard XML parse error: " + e);
    277             throw new IllegalArgumentException(e);
    278         } catch (IOException e) {
    279             Log.w(TAG, "keyboard XML parse error: " + e);
    280             throw new RuntimeException(e);
    281         } finally {
    282             parser.close();
    283         }
    284         return this;
    285     }
    286 
    287     public void setTouchPositionCorrectionEnabled(boolean enabled) {
    288         mParams.mTouchPositionCorrection.setEnabled(enabled);
    289     }
    290 
    291     public Keyboard build() {
    292         return new Keyboard(mParams);
    293     }
    294 
    295     private void parseKeyboard(XmlResourceParser parser)
    296             throws XmlPullParserException, IOException {
    297         if (DEBUG) Log.d(TAG, String.format("<%s> %s", TAG_KEYBOARD, mParams.mId));
    298         int event;
    299         while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
    300             if (event == XmlPullParser.START_TAG) {
    301                 final String tag = parser.getName();
    302                 if (TAG_KEYBOARD.equals(tag)) {
    303                     parseKeyboardAttributes(parser);
    304                     startKeyboard();
    305                     parseKeyboardContent(parser, false);
    306                     break;
    307                 } else {
    308                     throw new IllegalStartTag(parser, TAG_KEYBOARD);
    309                 }
    310             }
    311         }
    312     }
    313 
    314     public static String parseKeyboardLocale(
    315             Context context, int resId) throws XmlPullParserException, IOException {
    316         final Resources res = context.getResources();
    317         final XmlPullParser parser = res.getXml(resId);
    318         if (parser == null) return "";
    319         int event;
    320         while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
    321             if (event == XmlPullParser.START_TAG) {
    322                 final String tag = parser.getName();
    323                 if (TAG_KEYBOARD.equals(tag)) {
    324                     final TypedArray keyboardAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
    325                             R.styleable.Keyboard);
    326                     final String locale = keyboardAttr.getString(
    327                             R.styleable.Keyboard_keyboardLocale);
    328                     keyboardAttr.recycle();
    329                     return locale;
    330                 } else {
    331                     throw new IllegalStartTag(parser, TAG_KEYBOARD);
    332                 }
    333             }
    334         }
    335         return "";
    336     }
    337 
    338     private void parseKeyboardAttributes(XmlPullParser parser) {
    339         final int displayWidth = mDisplayMetrics.widthPixels;
    340         final TypedArray keyboardAttr = mContext.obtainStyledAttributes(
    341                 Xml.asAttributeSet(parser), R.styleable.Keyboard, R.attr.keyboardStyle,
    342                 R.style.Keyboard);
    343         final TypedArray keyAttr = mResources.obtainAttributes(Xml.asAttributeSet(parser),
    344                 R.styleable.Keyboard_Key);
    345         try {
    346             final int displayHeight = mDisplayMetrics.heightPixels;
    347             final int keyboardHeight = (int)keyboardAttr.getDimension(
    348                     R.styleable.Keyboard_keyboardHeight, displayHeight / 2);
    349             final int maxKeyboardHeight = (int)getDimensionOrFraction(keyboardAttr,
    350                     R.styleable.Keyboard_maxKeyboardHeight, displayHeight, displayHeight / 2);
    351             int minKeyboardHeight = (int)getDimensionOrFraction(keyboardAttr,
    352                     R.styleable.Keyboard_minKeyboardHeight, displayHeight, displayHeight / 2);
    353             if (minKeyboardHeight < 0) {
    354                 // Specified fraction was negative, so it should be calculated against display
    355                 // width.
    356                 minKeyboardHeight = -(int)getDimensionOrFraction(keyboardAttr,
    357                         R.styleable.Keyboard_minKeyboardHeight, displayWidth, displayWidth / 2);
    358             }
    359             final KeyboardParams params = mParams;
    360             // Keyboard height will not exceed maxKeyboardHeight and will not be less than
    361             // minKeyboardHeight.
    362             params.mOccupiedHeight = Math.max(
    363                     Math.min(keyboardHeight, maxKeyboardHeight), minKeyboardHeight);
    364             params.mOccupiedWidth = params.mId.mWidth;
    365             params.mTopPadding = (int)getDimensionOrFraction(keyboardAttr,
    366                     R.styleable.Keyboard_keyboardTopPadding, params.mOccupiedHeight, 0);
    367             params.mBottomPadding = (int)getDimensionOrFraction(keyboardAttr,
    368                     R.styleable.Keyboard_keyboardBottomPadding, params.mOccupiedHeight, 0);
    369             params.mHorizontalEdgesPadding = (int)getDimensionOrFraction(keyboardAttr,
    370                     R.styleable.Keyboard_keyboardHorizontalEdgesPadding, mParams.mOccupiedWidth, 0);
    371 
    372             params.mBaseWidth = params.mOccupiedWidth - params.mHorizontalEdgesPadding * 2
    373                     - params.mHorizontalCenterPadding;
    374             params.mDefaultKeyWidth = (int)getDimensionOrFraction(keyAttr,
    375                     R.styleable.Keyboard_Key_keyWidth, params.mBaseWidth,
    376                     params.mBaseWidth / DEFAULT_KEYBOARD_COLUMNS);
    377             params.mHorizontalGap = (int)getDimensionOrFraction(keyboardAttr,
    378                     R.styleable.Keyboard_horizontalGap, params.mBaseWidth, 0);
    379             params.mVerticalGap = (int)getDimensionOrFraction(keyboardAttr,
    380                     R.styleable.Keyboard_verticalGap, params.mOccupiedHeight, 0);
    381             params.mBaseHeight = params.mOccupiedHeight - params.mTopPadding
    382                     - params.mBottomPadding + params.mVerticalGap;
    383             params.mDefaultRowHeight = (int)getDimensionOrFraction(keyboardAttr,
    384                     R.styleable.Keyboard_rowHeight, params.mBaseHeight,
    385                     params.mBaseHeight / DEFAULT_KEYBOARD_ROWS);
    386 
    387             params.mIsRtlKeyboard = keyboardAttr.getBoolean(
    388                     R.styleable.Keyboard_isRtlKeyboard, false);
    389             params.mMoreKeysTemplate = keyboardAttr.getResourceId(
    390                     R.styleable.Keyboard_moreKeysTemplate, 0);
    391             params.mMaxMiniKeyboardColumn = keyAttr.getInt(
    392                     R.styleable.Keyboard_Key_maxMoreKeysColumn, 5);
    393 
    394             params.mIconsSet.loadIcons(keyboardAttr);
    395         } finally {
    396             keyAttr.recycle();
    397             keyboardAttr.recycle();
    398         }
    399     }
    400 
    401     private void parseKeyboardContent(XmlPullParser parser, boolean skip)
    402             throws XmlPullParserException, IOException {
    403         int event;
    404         while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
    405             if (event == XmlPullParser.START_TAG) {
    406                 final String tag = parser.getName();
    407                 if (TAG_ROW.equals(tag)) {
    408                     Row row = parseRowAttributes(parser);
    409                     if (DEBUG) Log.d(TAG, String.format("<%s>", TAG_ROW));
    410                     if (!skip)
    411                         startRow(row);
    412                     parseRowContent(parser, row, skip);
    413                 } else if (TAG_INCLUDE.equals(tag)) {
    414                     parseIncludeKeyboardContent(parser, skip);
    415                 } else if (TAG_SWITCH.equals(tag)) {
    416                     parseSwitchKeyboardContent(parser, skip);
    417                 } else if (TAG_KEY_STYLE.equals(tag)) {
    418                     parseKeyStyle(parser, skip);
    419                 } else {
    420                     throw new IllegalStartTag(parser, TAG_ROW);
    421                 }
    422             } else if (event == XmlPullParser.END_TAG) {
    423                 final String tag = parser.getName();
    424                 if (TAG_KEYBOARD.equals(tag)) {
    425                     endKeyboard();
    426                     break;
    427                 } else if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag)
    428                         || TAG_MERGE.equals(tag)) {
    429                     if (DEBUG) Log.d(TAG, String.format("</%s>", tag));
    430                     break;
    431                 } else if (TAG_KEY_STYLE.equals(tag)) {
    432                     continue;
    433                 } else {
    434                     throw new IllegalEndTag(parser, TAG_ROW);
    435                 }
    436             }
    437         }
    438     }
    439 
    440     private Row parseRowAttributes(XmlPullParser parser) {
    441         final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
    442                 R.styleable.Keyboard);
    443         try {
    444             if (a.hasValue(R.styleable.Keyboard_horizontalGap))
    445                 throw new IllegalAttribute(parser, "horizontalGap");
    446             if (a.hasValue(R.styleable.Keyboard_verticalGap))
    447                 throw new IllegalAttribute(parser, "verticalGap");
    448             return new Row(mResources, mParams, parser, mCurrentY);
    449         } finally {
    450             a.recycle();
    451         }
    452     }
    453 
    454     private void parseRowContent(XmlPullParser parser, Row row, boolean skip)
    455             throws XmlPullParserException, IOException {
    456         int event;
    457         while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
    458             if (event == XmlPullParser.START_TAG) {
    459                 final String tag = parser.getName();
    460                 if (TAG_KEY.equals(tag)) {
    461                     parseKey(parser, row, skip);
    462                 } else if (TAG_SPACER.equals(tag)) {
    463                     parseSpacer(parser, row, skip);
    464                 } else if (TAG_INCLUDE.equals(tag)) {
    465                     parseIncludeRowContent(parser, row, skip);
    466                 } else if (TAG_SWITCH.equals(tag)) {
    467                     parseSwitchRowContent(parser, row, skip);
    468                 } else if (TAG_KEY_STYLE.equals(tag)) {
    469                     parseKeyStyle(parser, skip);
    470                 } else {
    471                     throw new IllegalStartTag(parser, TAG_KEY);
    472                 }
    473             } else if (event == XmlPullParser.END_TAG) {
    474                 final String tag = parser.getName();
    475                 if (TAG_ROW.equals(tag)) {
    476                     if (DEBUG) Log.d(TAG, String.format("</%s>", TAG_ROW));
    477                     if (!skip)
    478                         endRow(row);
    479                     break;
    480                 } else if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag)
    481                         || TAG_MERGE.equals(tag)) {
    482                     if (DEBUG) Log.d(TAG, String.format("</%s>", tag));
    483                     break;
    484                 } else if (TAG_KEY_STYLE.equals(tag)) {
    485                     continue;
    486                 } else {
    487                     throw new IllegalEndTag(parser, TAG_KEY);
    488                 }
    489             }
    490         }
    491     }
    492 
    493     private void parseKey(XmlPullParser parser, Row row, boolean skip)
    494             throws XmlPullParserException, IOException {
    495         if (skip) {
    496             checkEndTag(TAG_KEY, parser);
    497         } else {
    498             final Key key = new Key(mResources, mParams, row, parser, mKeyStyles);
    499             if (DEBUG) Log.d(TAG, String.format("<%s%s keyLabel=%s code=%d moreKeys=%s />",
    500                     TAG_KEY, (key.isEnabled() ? "" : " disabled"), key.mLabel, key.mCode,
    501                     Arrays.toString(key.mMoreKeys)));
    502             checkEndTag(TAG_KEY, parser);
    503             endKey(key);
    504         }
    505     }
    506 
    507     private void parseSpacer(XmlPullParser parser, Row row, boolean skip)
    508             throws XmlPullParserException, IOException {
    509         if (skip) {
    510             checkEndTag(TAG_SPACER, parser);
    511         } else {
    512             final Key.Spacer spacer = new Key.Spacer(mResources, mParams, row, parser, mKeyStyles);
    513             if (DEBUG) Log.d(TAG, String.format("<%s />", TAG_SPACER));
    514             checkEndTag(TAG_SPACER, parser);
    515             endKey(spacer);
    516         }
    517     }
    518 
    519     private void parseIncludeKeyboardContent(XmlPullParser parser, boolean skip)
    520             throws XmlPullParserException, IOException {
    521         parseIncludeInternal(parser, null, skip);
    522     }
    523 
    524     private void parseIncludeRowContent(XmlPullParser parser, Row row, boolean skip)
    525             throws XmlPullParserException, IOException {
    526         parseIncludeInternal(parser, row, skip);
    527     }
    528 
    529     private void parseIncludeInternal(XmlPullParser parser, Row row, boolean skip)
    530             throws XmlPullParserException, IOException {
    531         if (skip) {
    532             checkEndTag(TAG_INCLUDE, parser);
    533         } else {
    534             final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
    535                     R.styleable.Keyboard_Include);
    536             final int keyboardLayout = a.getResourceId(
    537                     R.styleable.Keyboard_Include_keyboardLayout, 0);
    538             a.recycle();
    539 
    540             checkEndTag(TAG_INCLUDE, parser);
    541             if (keyboardLayout == 0)
    542                 throw new ParseException("No keyboardLayout attribute in <include/>", parser);
    543             if (DEBUG) Log.d(TAG, String.format("<%s keyboardLayout=%s />",
    544                     TAG_INCLUDE, mResources.getResourceEntryName(keyboardLayout)));
    545             final XmlResourceParser parserForInclude = mResources.getXml(keyboardLayout);
    546             try {
    547                 parseMerge(parserForInclude, row, skip);
    548             } finally {
    549                 parserForInclude.close();
    550             }
    551         }
    552     }
    553 
    554     private void parseMerge(XmlPullParser parser, Row row, boolean skip)
    555             throws XmlPullParserException, IOException {
    556         int event;
    557         while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
    558             if (event == XmlPullParser.START_TAG) {
    559                 final String tag = parser.getName();
    560                 if (TAG_MERGE.equals(tag)) {
    561                     if (row == null) {
    562                         parseKeyboardContent(parser, skip);
    563                     } else {
    564                         parseRowContent(parser, row, skip);
    565                     }
    566                     break;
    567                 } else {
    568                     throw new ParseException(
    569                             "Included keyboard layout must have <merge> root element", parser);
    570                 }
    571             }
    572         }
    573     }
    574 
    575     private void parseSwitchKeyboardContent(XmlPullParser parser, boolean skip)
    576             throws XmlPullParserException, IOException {
    577         parseSwitchInternal(parser, null, skip);
    578     }
    579 
    580     private void parseSwitchRowContent(XmlPullParser parser, Row row, boolean skip)
    581             throws XmlPullParserException, IOException {
    582         parseSwitchInternal(parser, row, skip);
    583     }
    584 
    585     private void parseSwitchInternal(XmlPullParser parser, Row row, boolean skip)
    586             throws XmlPullParserException, IOException {
    587         if (DEBUG) Log.d(TAG, String.format("<%s> %s", TAG_SWITCH, mParams.mId));
    588         boolean selected = false;
    589         int event;
    590         while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
    591             if (event == XmlPullParser.START_TAG) {
    592                 final String tag = parser.getName();
    593                 if (TAG_CASE.equals(tag)) {
    594                     selected |= parseCase(parser, row, selected ? true : skip);
    595                 } else if (TAG_DEFAULT.equals(tag)) {
    596                     selected |= parseDefault(parser, row, selected ? true : skip);
    597                 } else {
    598                     throw new IllegalStartTag(parser, TAG_KEY);
    599                 }
    600             } else if (event == XmlPullParser.END_TAG) {
    601                 final String tag = parser.getName();
    602                 if (TAG_SWITCH.equals(tag)) {
    603                     if (DEBUG) Log.d(TAG, String.format("</%s>", TAG_SWITCH));
    604                     break;
    605                 } else {
    606                     throw new IllegalEndTag(parser, TAG_KEY);
    607                 }
    608             }
    609         }
    610     }
    611 
    612     private boolean parseCase(XmlPullParser parser, Row row, boolean skip)
    613             throws XmlPullParserException, IOException {
    614         final boolean selected = parseCaseCondition(parser);
    615         if (row == null) {
    616             // Processing Rows.
    617             parseKeyboardContent(parser, selected ? skip : true);
    618         } else {
    619             // Processing Keys.
    620             parseRowContent(parser, row, selected ? skip : true);
    621         }
    622         return selected;
    623     }
    624 
    625     private boolean parseCaseCondition(XmlPullParser parser) {
    626         final KeyboardId id = mParams.mId;
    627         if (id == null)
    628             return true;
    629 
    630         final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
    631                 R.styleable.Keyboard_Case);
    632         try {
    633             final boolean modeMatched = matchTypedValue(a,
    634                     R.styleable.Keyboard_Case_mode, id.mMode, KeyboardId.modeName(id.mMode));
    635             final boolean navigateActionMatched = matchBoolean(a,
    636                     R.styleable.Keyboard_Case_navigateAction, id.mNavigateAction);
    637             final boolean passwordInputMatched = matchBoolean(a,
    638                     R.styleable.Keyboard_Case_passwordInput, id.mPasswordInput);
    639             final boolean hasSettingsKeyMatched = matchBoolean(a,
    640                     R.styleable.Keyboard_Case_hasSettingsKey, id.mHasSettingsKey);
    641             final boolean f2KeyModeMatched = matchInteger(a,
    642                     R.styleable.Keyboard_Case_f2KeyMode, id.mF2KeyMode);
    643             final boolean clobberSettingsKeyMatched = matchBoolean(a,
    644                     R.styleable.Keyboard_Case_clobberSettingsKey, id.mClobberSettingsKey);
    645             final boolean shortcutKeyEnabledMatched = matchBoolean(a,
    646                     R.styleable.Keyboard_Case_shortcutKeyEnabled, id.mShortcutKeyEnabled);
    647             final boolean hasShortcutKeyMatched = matchBoolean(a,
    648                     R.styleable.Keyboard_Case_hasShortcutKey, id.mHasShortcutKey);
    649             // As noted at {@link KeyboardId} class, we are interested only in enum value masked by
    650             // {@link android.view.inputmethod.EditorInfo#IME_MASK_ACTION} and
    651             // {@link android.view.inputmethod.EditorInfo#IME_FLAG_NO_ENTER_ACTION}. So matching
    652             // this attribute with id.mImeOptions as integer value is enough for our purpose.
    653             final boolean imeActionMatched = matchInteger(a,
    654                     R.styleable.Keyboard_Case_imeAction, id.mImeAction);
    655             final boolean localeCodeMatched = matchString(a,
    656                     R.styleable.Keyboard_Case_localeCode, id.mLocale.toString());
    657             final boolean languageCodeMatched = matchString(a,
    658                     R.styleable.Keyboard_Case_languageCode, id.mLocale.getLanguage());
    659             final boolean countryCodeMatched = matchString(a,
    660                     R.styleable.Keyboard_Case_countryCode, id.mLocale.getCountry());
    661             final boolean selected = modeMatched && navigateActionMatched && passwordInputMatched
    662                     && hasSettingsKeyMatched && f2KeyModeMatched && clobberSettingsKeyMatched
    663                     && shortcutKeyEnabledMatched && hasShortcutKeyMatched && imeActionMatched &&
    664                     localeCodeMatched && languageCodeMatched && countryCodeMatched;
    665 
    666             if (DEBUG) Log.d(TAG, String.format("<%s%s%s%s%s%s%s%s%s%s%s%s%s> %s", TAG_CASE,
    667                     textAttr(a.getString(R.styleable.Keyboard_Case_mode), "mode"),
    668                     booleanAttr(a, R.styleable.Keyboard_Case_navigateAction, "navigateAction"),
    669                     booleanAttr(a, R.styleable.Keyboard_Case_passwordInput, "passwordInput"),
    670                     booleanAttr(a, R.styleable.Keyboard_Case_hasSettingsKey, "hasSettingsKey"),
    671                     textAttr(KeyboardId.f2KeyModeName(
    672                             a.getInt(R.styleable.Keyboard_Case_f2KeyMode, -1)), "f2KeyMode"),
    673                     booleanAttr(a, R.styleable.Keyboard_Case_clobberSettingsKey,
    674                             "clobberSettingsKey"),
    675                     booleanAttr(
    676                             a, R.styleable.Keyboard_Case_shortcutKeyEnabled, "shortcutKeyEnabled"),
    677                     booleanAttr(a, R.styleable.Keyboard_Case_hasShortcutKey, "hasShortcutKey"),
    678                     textAttr(EditorInfoCompatUtils.imeOptionsName(
    679                             a.getInt(R.styleable.Keyboard_Case_imeAction, -1)), "imeAction"),
    680                     textAttr(a.getString(R.styleable.Keyboard_Case_localeCode), "localeCode"),
    681                     textAttr(a.getString(R.styleable.Keyboard_Case_languageCode), "languageCode"),
    682                     textAttr(a.getString(R.styleable.Keyboard_Case_countryCode), "countryCode"),
    683                     Boolean.toString(selected)));
    684 
    685             return selected;
    686         } finally {
    687             a.recycle();
    688         }
    689     }
    690 
    691     private static boolean matchInteger(TypedArray a, int index, int value) {
    692         // If <case> does not have "index" attribute, that means this <case> is wild-card for the
    693         // attribute.
    694         return !a.hasValue(index) || a.getInt(index, 0) == value;
    695     }
    696 
    697     private static boolean matchBoolean(TypedArray a, int index, boolean value) {
    698         // If <case> does not have "index" attribute, that means this <case> is wild-card for the
    699         // attribute.
    700         return !a.hasValue(index) || a.getBoolean(index, false) == value;
    701     }
    702 
    703     private static boolean matchString(TypedArray a, int index, String value) {
    704         // If <case> does not have "index" attribute, that means this <case> is wild-card for the
    705         // attribute.
    706         return !a.hasValue(index) || stringArrayContains(a.getString(index).split("\\|"), value);
    707     }
    708 
    709     private static boolean matchTypedValue(TypedArray a, int index, int intValue, String strValue) {
    710         // If <case> does not have "index" attribute, that means this <case> is wild-card for the
    711         // attribute.
    712         final TypedValue v = a.peekValue(index);
    713         if (v == null)
    714             return true;
    715 
    716         if (isIntegerValue(v)) {
    717             return intValue == a.getInt(index, 0);
    718         } else if (isStringValue(v)) {
    719             return stringArrayContains(a.getString(index).split("\\|"), strValue);
    720         }
    721         return false;
    722     }
    723 
    724     private static boolean stringArrayContains(String[] array, String value) {
    725         for (final String elem : array) {
    726             if (elem.equals(value))
    727                 return true;
    728         }
    729         return false;
    730     }
    731 
    732     private boolean parseDefault(XmlPullParser parser, Row row, boolean skip)
    733             throws XmlPullParserException, IOException {
    734         if (DEBUG) Log.d(TAG, String.format("<%s>", TAG_DEFAULT));
    735         if (row == null) {
    736             parseKeyboardContent(parser, skip);
    737         } else {
    738             parseRowContent(parser, row, skip);
    739         }
    740         return true;
    741     }
    742 
    743     private void parseKeyStyle(XmlPullParser parser, boolean skip) {
    744         TypedArray keyStyleAttr = mResources.obtainAttributes(Xml.asAttributeSet(parser),
    745                 R.styleable.Keyboard_KeyStyle);
    746         TypedArray keyAttrs = mResources.obtainAttributes(Xml.asAttributeSet(parser),
    747                 R.styleable.Keyboard_Key);
    748         try {
    749             if (!keyStyleAttr.hasValue(R.styleable.Keyboard_KeyStyle_styleName))
    750                 throw new ParseException("<" + TAG_KEY_STYLE
    751                         + "/> needs styleName attribute", parser);
    752             if (!skip)
    753                 mKeyStyles.parseKeyStyleAttributes(keyStyleAttr, keyAttrs, parser);
    754         } finally {
    755             keyStyleAttr.recycle();
    756             keyAttrs.recycle();
    757         }
    758     }
    759 
    760     private static void checkEndTag(String tag, XmlPullParser parser)
    761             throws XmlPullParserException, IOException {
    762         if (parser.next() == XmlPullParser.END_TAG && tag.equals(parser.getName()))
    763             return;
    764         throw new NonEmptyTag(tag, parser);
    765     }
    766 
    767     private void startKeyboard() {
    768         mCurrentY += mParams.mTopPadding;
    769         mTopEdge = true;
    770     }
    771 
    772     private void startRow(Row row) {
    773         addEdgeSpace(mParams.mHorizontalEdgesPadding, row);
    774         mCurrentRow = row;
    775         mLeftEdge = true;
    776         mRightEdgeKey = null;
    777     }
    778 
    779     private void endRow(Row row) {
    780         if (mCurrentRow == null)
    781             throw new InflateException("orphant end row tag");
    782         if (mRightEdgeKey != null) {
    783             mRightEdgeKey.markAsRightEdge(mParams);
    784             mRightEdgeKey = null;
    785         }
    786         addEdgeSpace(mParams.mHorizontalEdgesPadding, row);
    787         mCurrentY += row.mRowHeight;
    788         mCurrentRow = null;
    789         mTopEdge = false;
    790     }
    791 
    792     private void endKey(Key key) {
    793         mParams.onAddKey(key);
    794         if (mLeftEdge) {
    795             key.markAsLeftEdge(mParams);
    796             mLeftEdge = false;
    797         }
    798         if (mTopEdge) {
    799             key.markAsTopEdge(mParams);
    800         }
    801         mRightEdgeKey = key;
    802     }
    803 
    804     private void endKeyboard() {
    805     }
    806 
    807     private void addEdgeSpace(float width, Row row) {
    808         row.advanceXPos(width);
    809         mLeftEdge = false;
    810         mRightEdgeKey = null;
    811     }
    812 
    813     public static float getDimensionOrFraction(TypedArray a, int index, int base, float defValue) {
    814         final TypedValue value = a.peekValue(index);
    815         if (value == null)
    816             return defValue;
    817         if (isFractionValue(value)) {
    818             return a.getFraction(index, base, base, defValue);
    819         } else if (isDimensionValue(value)) {
    820             return a.getDimension(index, defValue);
    821         }
    822         return defValue;
    823     }
    824 
    825     public static int getEnumValue(TypedArray a, int index, int defValue) {
    826         final TypedValue value = a.peekValue(index);
    827         if (value == null)
    828             return defValue;
    829         if (isIntegerValue(value)) {
    830             return a.getInt(index, defValue);
    831         }
    832         return defValue;
    833     }
    834 
    835     private static boolean isFractionValue(TypedValue v) {
    836         return v.type == TypedValue.TYPE_FRACTION;
    837     }
    838 
    839     private static boolean isDimensionValue(TypedValue v) {
    840         return v.type == TypedValue.TYPE_DIMENSION;
    841     }
    842 
    843     private static boolean isIntegerValue(TypedValue v) {
    844         return v.type >= TypedValue.TYPE_FIRST_INT && v.type <= TypedValue.TYPE_LAST_INT;
    845     }
    846 
    847     private static boolean isStringValue(TypedValue v) {
    848         return v.type == TypedValue.TYPE_STRING;
    849     }
    850 
    851     @SuppressWarnings("serial")
    852     public static class ParseException extends InflateException {
    853         public ParseException(String msg, XmlPullParser parser) {
    854             super(msg + " at line " + parser.getLineNumber());
    855         }
    856     }
    857 
    858     @SuppressWarnings("serial")
    859     private static class IllegalStartTag extends ParseException {
    860         public IllegalStartTag(XmlPullParser parser, String parent) {
    861             super("Illegal start tag " + parser.getName() + " in " + parent, parser);
    862         }
    863     }
    864 
    865     @SuppressWarnings("serial")
    866     private static class IllegalEndTag extends ParseException {
    867         public IllegalEndTag(XmlPullParser parser, String parent) {
    868             super("Illegal end tag " + parser.getName() + " in " + parent, parser);
    869         }
    870     }
    871 
    872     @SuppressWarnings("serial")
    873     private static class IllegalAttribute extends ParseException {
    874         public IllegalAttribute(XmlPullParser parser, String attribute) {
    875             super("Tag " + parser.getName() + " has illegal attribute " + attribute, parser);
    876         }
    877     }
    878 
    879     @SuppressWarnings("serial")
    880     private static class NonEmptyTag extends ParseException {
    881         public NonEmptyTag(String tag, XmlPullParser parser) {
    882             super(tag + " must be empty tag", parser);
    883         }
    884     }
    885 
    886     private static String textAttr(String value, String name) {
    887         return value != null ? String.format(" %s=%s", name, value) : "";
    888     }
    889 
    890     private static String booleanAttr(TypedArray a, int index, String name) {
    891         return a.hasValue(index) ? String.format(" %s=%s", name, a.getBoolean(index, false)) : "";
    892     }
    893 }
    894