KeyboardBuilder.java revision 4665463ccd4880c2392d26baa784c5d28de986cc
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
17package com.android.inputmethod.keyboard.internal;
18
19import android.content.Context;
20import android.content.res.Resources;
21import android.content.res.TypedArray;
22import android.content.res.XmlResourceParser;
23import android.util.DisplayMetrics;
24import android.util.Log;
25import android.util.TypedValue;
26import android.util.Xml;
27import android.view.InflateException;
28
29import com.android.inputmethod.compat.EditorInfoCompatUtils;
30import com.android.inputmethod.keyboard.Key;
31import com.android.inputmethod.keyboard.Keyboard;
32import com.android.inputmethod.keyboard.KeyboardId;
33import com.android.inputmethod.latin.LatinImeLogger;
34import com.android.inputmethod.latin.R;
35
36import org.xmlpull.v1.XmlPullParser;
37import org.xmlpull.v1.XmlPullParserException;
38
39import java.io.IOException;
40import 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
111public 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(XmlPullParser 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 XmlParseUtils.IllegalStartTag(parser, TAG_KEYBOARD);
309                }
310            }
311        }
312    }
313
314    private void parseKeyboardAttributes(XmlPullParser parser) {
315        final int displayWidth = mDisplayMetrics.widthPixels;
316        final TypedArray keyboardAttr = mContext.obtainStyledAttributes(
317                Xml.asAttributeSet(parser), R.styleable.Keyboard, R.attr.keyboardStyle,
318                R.style.Keyboard);
319        final TypedArray keyAttr = mResources.obtainAttributes(Xml.asAttributeSet(parser),
320                R.styleable.Keyboard_Key);
321        try {
322            final int displayHeight = mDisplayMetrics.heightPixels;
323            final int keyboardHeight = (int)keyboardAttr.getDimension(
324                    R.styleable.Keyboard_keyboardHeight, displayHeight / 2);
325            final int maxKeyboardHeight = (int)getDimensionOrFraction(keyboardAttr,
326                    R.styleable.Keyboard_maxKeyboardHeight, displayHeight, displayHeight / 2);
327            int minKeyboardHeight = (int)getDimensionOrFraction(keyboardAttr,
328                    R.styleable.Keyboard_minKeyboardHeight, displayHeight, displayHeight / 2);
329            if (minKeyboardHeight < 0) {
330                // Specified fraction was negative, so it should be calculated against display
331                // width.
332                minKeyboardHeight = -(int)getDimensionOrFraction(keyboardAttr,
333                        R.styleable.Keyboard_minKeyboardHeight, displayWidth, displayWidth / 2);
334            }
335            final KeyboardParams params = mParams;
336            // Keyboard height will not exceed maxKeyboardHeight and will not be less than
337            // minKeyboardHeight.
338            params.mOccupiedHeight = Math.max(
339                    Math.min(keyboardHeight, maxKeyboardHeight), minKeyboardHeight);
340            params.mOccupiedWidth = params.mId.mWidth;
341            params.mTopPadding = (int)getDimensionOrFraction(keyboardAttr,
342                    R.styleable.Keyboard_keyboardTopPadding, params.mOccupiedHeight, 0);
343            params.mBottomPadding = (int)getDimensionOrFraction(keyboardAttr,
344                    R.styleable.Keyboard_keyboardBottomPadding, params.mOccupiedHeight, 0);
345            params.mHorizontalEdgesPadding = (int)getDimensionOrFraction(keyboardAttr,
346                    R.styleable.Keyboard_keyboardHorizontalEdgesPadding, mParams.mOccupiedWidth, 0);
347
348            params.mBaseWidth = params.mOccupiedWidth - params.mHorizontalEdgesPadding * 2
349                    - params.mHorizontalCenterPadding;
350            params.mDefaultKeyWidth = (int)getDimensionOrFraction(keyAttr,
351                    R.styleable.Keyboard_Key_keyWidth, params.mBaseWidth,
352                    params.mBaseWidth / DEFAULT_KEYBOARD_COLUMNS);
353            params.mHorizontalGap = (int)getDimensionOrFraction(keyboardAttr,
354                    R.styleable.Keyboard_horizontalGap, params.mBaseWidth, 0);
355            params.mVerticalGap = (int)getDimensionOrFraction(keyboardAttr,
356                    R.styleable.Keyboard_verticalGap, params.mOccupiedHeight, 0);
357            params.mBaseHeight = params.mOccupiedHeight - params.mTopPadding
358                    - params.mBottomPadding + params.mVerticalGap;
359            params.mDefaultRowHeight = (int)getDimensionOrFraction(keyboardAttr,
360                    R.styleable.Keyboard_rowHeight, params.mBaseHeight,
361                    params.mBaseHeight / DEFAULT_KEYBOARD_ROWS);
362
363            params.mIsRtlKeyboard = keyboardAttr.getBoolean(
364                    R.styleable.Keyboard_isRtlKeyboard, false);
365            params.mMoreKeysTemplate = keyboardAttr.getResourceId(
366                    R.styleable.Keyboard_moreKeysTemplate, 0);
367            params.mMaxMiniKeyboardColumn = keyAttr.getInt(
368                    R.styleable.Keyboard_Key_maxMoreKeysColumn, 5);
369
370            params.mIconsSet.loadIcons(keyboardAttr);
371        } finally {
372            keyAttr.recycle();
373            keyboardAttr.recycle();
374        }
375    }
376
377    private void parseKeyboardContent(XmlPullParser parser, boolean skip)
378            throws XmlPullParserException, IOException {
379        int event;
380        while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
381            if (event == XmlPullParser.START_TAG) {
382                final String tag = parser.getName();
383                if (TAG_ROW.equals(tag)) {
384                    Row row = parseRowAttributes(parser);
385                    if (DEBUG) Log.d(TAG, String.format("<%s>", TAG_ROW));
386                    if (!skip)
387                        startRow(row);
388                    parseRowContent(parser, row, skip);
389                } else if (TAG_INCLUDE.equals(tag)) {
390                    parseIncludeKeyboardContent(parser, skip);
391                } else if (TAG_SWITCH.equals(tag)) {
392                    parseSwitchKeyboardContent(parser, skip);
393                } else if (TAG_KEY_STYLE.equals(tag)) {
394                    parseKeyStyle(parser, skip);
395                } else {
396                    throw new XmlParseUtils.IllegalStartTag(parser, TAG_ROW);
397                }
398            } else if (event == XmlPullParser.END_TAG) {
399                final String tag = parser.getName();
400                if (TAG_KEYBOARD.equals(tag)) {
401                    endKeyboard();
402                    break;
403                } else if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag)
404                        || TAG_MERGE.equals(tag)) {
405                    if (DEBUG) Log.d(TAG, String.format("</%s>", tag));
406                    break;
407                } else if (TAG_KEY_STYLE.equals(tag)) {
408                    continue;
409                } else {
410                    throw new XmlParseUtils.IllegalEndTag(parser, TAG_ROW);
411                }
412            }
413        }
414    }
415
416    private Row parseRowAttributes(XmlPullParser parser) throws XmlPullParserException {
417        final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
418                R.styleable.Keyboard);
419        try {
420            if (a.hasValue(R.styleable.Keyboard_horizontalGap))
421                throw new XmlParseUtils.IllegalAttribute(parser, "horizontalGap");
422            if (a.hasValue(R.styleable.Keyboard_verticalGap))
423                throw new XmlParseUtils.IllegalAttribute(parser, "verticalGap");
424            return new Row(mResources, mParams, parser, mCurrentY);
425        } finally {
426            a.recycle();
427        }
428    }
429
430    private void parseRowContent(XmlPullParser parser, Row row, boolean skip)
431            throws XmlPullParserException, IOException {
432        int event;
433        while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
434            if (event == XmlPullParser.START_TAG) {
435                final String tag = parser.getName();
436                if (TAG_KEY.equals(tag)) {
437                    parseKey(parser, row, skip);
438                } else if (TAG_SPACER.equals(tag)) {
439                    parseSpacer(parser, row, skip);
440                } else if (TAG_INCLUDE.equals(tag)) {
441                    parseIncludeRowContent(parser, row, skip);
442                } else if (TAG_SWITCH.equals(tag)) {
443                    parseSwitchRowContent(parser, row, skip);
444                } else if (TAG_KEY_STYLE.equals(tag)) {
445                    parseKeyStyle(parser, skip);
446                } else {
447                    throw new XmlParseUtils.IllegalStartTag(parser, TAG_KEY);
448                }
449            } else if (event == XmlPullParser.END_TAG) {
450                final String tag = parser.getName();
451                if (TAG_ROW.equals(tag)) {
452                    if (DEBUG) Log.d(TAG, String.format("</%s>", TAG_ROW));
453                    if (!skip)
454                        endRow(row);
455                    break;
456                } else if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag)
457                        || TAG_MERGE.equals(tag)) {
458                    if (DEBUG) Log.d(TAG, String.format("</%s>", tag));
459                    break;
460                } else if (TAG_KEY_STYLE.equals(tag)) {
461                    continue;
462                } else {
463                    throw new XmlParseUtils.IllegalEndTag(parser, TAG_KEY);
464                }
465            }
466        }
467    }
468
469    private void parseKey(XmlPullParser parser, Row row, boolean skip)
470            throws XmlPullParserException, IOException {
471        if (skip) {
472            XmlParseUtils.checkEndTag(TAG_KEY, parser);
473        } else {
474            final Key key = new Key(mResources, mParams, row, parser, mKeyStyles);
475            if (DEBUG) Log.d(TAG, String.format("<%s%s keyLabel=%s code=%d moreKeys=%s />",
476                    TAG_KEY, (key.isEnabled() ? "" : " disabled"), key.mLabel, key.mCode,
477                    Arrays.toString(key.mMoreKeys)));
478            XmlParseUtils.checkEndTag(TAG_KEY, parser);
479            endKey(key);
480        }
481    }
482
483    private void parseSpacer(XmlPullParser parser, Row row, boolean skip)
484            throws XmlPullParserException, IOException {
485        if (skip) {
486            XmlParseUtils.checkEndTag(TAG_SPACER, parser);
487        } else {
488            final Key.Spacer spacer = new Key.Spacer(mResources, mParams, row, parser, mKeyStyles);
489            if (DEBUG) Log.d(TAG, String.format("<%s />", TAG_SPACER));
490            XmlParseUtils.checkEndTag(TAG_SPACER, parser);
491            endKey(spacer);
492        }
493    }
494
495    private void parseIncludeKeyboardContent(XmlPullParser parser, boolean skip)
496            throws XmlPullParserException, IOException {
497        parseIncludeInternal(parser, null, skip);
498    }
499
500    private void parseIncludeRowContent(XmlPullParser parser, Row row, boolean skip)
501            throws XmlPullParserException, IOException {
502        parseIncludeInternal(parser, row, skip);
503    }
504
505    private void parseIncludeInternal(XmlPullParser parser, Row row, boolean skip)
506            throws XmlPullParserException, IOException {
507        if (skip) {
508            XmlParseUtils.checkEndTag(TAG_INCLUDE, parser);
509        } else {
510            final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
511                    R.styleable.Keyboard_Include);
512            int keyboardLayout = 0;
513            try {
514                XmlParseUtils.checkAttributeExists(a,
515                        R.styleable.Keyboard_Include_keyboardLayout, "keyboardLayout",
516                        TAG_INCLUDE, parser);
517                keyboardLayout = a.getResourceId(R.styleable.Keyboard_Include_keyboardLayout, 0);
518            } finally {
519                a.recycle();
520            }
521
522            XmlParseUtils.checkEndTag(TAG_INCLUDE, parser);
523            if (DEBUG) Log.d(TAG, String.format("<%s keyboardLayout=%s />",
524                    TAG_INCLUDE, mResources.getResourceEntryName(keyboardLayout)));
525            final XmlResourceParser parserForInclude = mResources.getXml(keyboardLayout);
526            try {
527                parseMerge(parserForInclude, row, skip);
528            } finally {
529                parserForInclude.close();
530            }
531        }
532    }
533
534    private void parseMerge(XmlPullParser parser, Row row, boolean skip)
535            throws XmlPullParserException, IOException {
536        int event;
537        while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
538            if (event == XmlPullParser.START_TAG) {
539                final String tag = parser.getName();
540                if (TAG_MERGE.equals(tag)) {
541                    if (row == null) {
542                        parseKeyboardContent(parser, skip);
543                    } else {
544                        parseRowContent(parser, row, skip);
545                    }
546                    break;
547                } else {
548                    throw new XmlParseUtils.ParseException(
549                            "Included keyboard layout must have <merge> root element", parser);
550                }
551            }
552        }
553    }
554
555    private void parseSwitchKeyboardContent(XmlPullParser parser, boolean skip)
556            throws XmlPullParserException, IOException {
557        parseSwitchInternal(parser, null, skip);
558    }
559
560    private void parseSwitchRowContent(XmlPullParser parser, Row row, boolean skip)
561            throws XmlPullParserException, IOException {
562        parseSwitchInternal(parser, row, skip);
563    }
564
565    private void parseSwitchInternal(XmlPullParser parser, Row row, boolean skip)
566            throws XmlPullParserException, IOException {
567        if (DEBUG) Log.d(TAG, String.format("<%s> %s", TAG_SWITCH, mParams.mId));
568        boolean selected = false;
569        int event;
570        while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
571            if (event == XmlPullParser.START_TAG) {
572                final String tag = parser.getName();
573                if (TAG_CASE.equals(tag)) {
574                    selected |= parseCase(parser, row, selected ? true : skip);
575                } else if (TAG_DEFAULT.equals(tag)) {
576                    selected |= parseDefault(parser, row, selected ? true : skip);
577                } else {
578                    throw new XmlParseUtils.IllegalStartTag(parser, TAG_KEY);
579                }
580            } else if (event == XmlPullParser.END_TAG) {
581                final String tag = parser.getName();
582                if (TAG_SWITCH.equals(tag)) {
583                    if (DEBUG) Log.d(TAG, String.format("</%s>", TAG_SWITCH));
584                    break;
585                } else {
586                    throw new XmlParseUtils.IllegalEndTag(parser, TAG_KEY);
587                }
588            }
589        }
590    }
591
592    private boolean parseCase(XmlPullParser parser, Row row, boolean skip)
593            throws XmlPullParserException, IOException {
594        final boolean selected = parseCaseCondition(parser);
595        if (row == null) {
596            // Processing Rows.
597            parseKeyboardContent(parser, selected ? skip : true);
598        } else {
599            // Processing Keys.
600            parseRowContent(parser, row, selected ? skip : true);
601        }
602        return selected;
603    }
604
605    private boolean parseCaseCondition(XmlPullParser parser) {
606        final KeyboardId id = mParams.mId;
607        if (id == null)
608            return true;
609
610        final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
611                R.styleable.Keyboard_Case);
612        try {
613            final boolean modeMatched = matchTypedValue(a,
614                    R.styleable.Keyboard_Case_mode, id.mMode, KeyboardId.modeName(id.mMode));
615            final boolean navigateActionMatched = matchBoolean(a,
616                    R.styleable.Keyboard_Case_navigateAction, id.mNavigateAction);
617            final boolean passwordInputMatched = matchBoolean(a,
618                    R.styleable.Keyboard_Case_passwordInput, id.mPasswordInput);
619            final boolean hasSettingsKeyMatched = matchBoolean(a,
620                    R.styleable.Keyboard_Case_hasSettingsKey, id.mHasSettingsKey);
621            final boolean f2KeyModeMatched = matchInteger(a,
622                    R.styleable.Keyboard_Case_f2KeyMode, id.mF2KeyMode);
623            final boolean clobberSettingsKeyMatched = matchBoolean(a,
624                    R.styleable.Keyboard_Case_clobberSettingsKey, id.mClobberSettingsKey);
625            final boolean shortcutKeyEnabledMatched = matchBoolean(a,
626                    R.styleable.Keyboard_Case_shortcutKeyEnabled, id.mShortcutKeyEnabled);
627            final boolean hasShortcutKeyMatched = matchBoolean(a,
628                    R.styleable.Keyboard_Case_hasShortcutKey, id.mHasShortcutKey);
629            // As noted at {@link KeyboardId} class, we are interested only in enum value masked by
630            // {@link android.view.inputmethod.EditorInfo#IME_MASK_ACTION} and
631            // {@link android.view.inputmethod.EditorInfo#IME_FLAG_NO_ENTER_ACTION}. So matching
632            // this attribute with id.mImeOptions as integer value is enough for our purpose.
633            final boolean imeActionMatched = matchInteger(a,
634                    R.styleable.Keyboard_Case_imeAction, id.mImeAction);
635            final boolean localeCodeMatched = matchString(a,
636                    R.styleable.Keyboard_Case_localeCode, id.mLocale.toString());
637            final boolean languageCodeMatched = matchString(a,
638                    R.styleable.Keyboard_Case_languageCode, id.mLocale.getLanguage());
639            final boolean countryCodeMatched = matchString(a,
640                    R.styleable.Keyboard_Case_countryCode, id.mLocale.getCountry());
641            final boolean selected = modeMatched && navigateActionMatched && passwordInputMatched
642                    && hasSettingsKeyMatched && f2KeyModeMatched && clobberSettingsKeyMatched
643                    && shortcutKeyEnabledMatched && hasShortcutKeyMatched && imeActionMatched &&
644                    localeCodeMatched && languageCodeMatched && countryCodeMatched;
645
646            if (DEBUG) Log.d(TAG, String.format("<%s%s%s%s%s%s%s%s%s%s%s%s%s> %s", TAG_CASE,
647                    textAttr(a.getString(R.styleable.Keyboard_Case_mode), "mode"),
648                    booleanAttr(a, R.styleable.Keyboard_Case_navigateAction, "navigateAction"),
649                    booleanAttr(a, R.styleable.Keyboard_Case_passwordInput, "passwordInput"),
650                    booleanAttr(a, R.styleable.Keyboard_Case_hasSettingsKey, "hasSettingsKey"),
651                    textAttr(KeyboardId.f2KeyModeName(
652                            a.getInt(R.styleable.Keyboard_Case_f2KeyMode, -1)), "f2KeyMode"),
653                    booleanAttr(a, R.styleable.Keyboard_Case_clobberSettingsKey,
654                            "clobberSettingsKey"),
655                    booleanAttr(
656                            a, R.styleable.Keyboard_Case_shortcutKeyEnabled, "shortcutKeyEnabled"),
657                    booleanAttr(a, R.styleable.Keyboard_Case_hasShortcutKey, "hasShortcutKey"),
658                    textAttr(EditorInfoCompatUtils.imeOptionsName(
659                            a.getInt(R.styleable.Keyboard_Case_imeAction, -1)), "imeAction"),
660                    textAttr(a.getString(R.styleable.Keyboard_Case_localeCode), "localeCode"),
661                    textAttr(a.getString(R.styleable.Keyboard_Case_languageCode), "languageCode"),
662                    textAttr(a.getString(R.styleable.Keyboard_Case_countryCode), "countryCode"),
663                    Boolean.toString(selected)));
664
665            return selected;
666        } finally {
667            a.recycle();
668        }
669    }
670
671    private static boolean matchInteger(TypedArray a, int index, int value) {
672        // If <case> does not have "index" attribute, that means this <case> is wild-card for the
673        // attribute.
674        return !a.hasValue(index) || a.getInt(index, 0) == value;
675    }
676
677    private static boolean matchBoolean(TypedArray a, int index, boolean value) {
678        // If <case> does not have "index" attribute, that means this <case> is wild-card for the
679        // attribute.
680        return !a.hasValue(index) || a.getBoolean(index, false) == value;
681    }
682
683    private static boolean matchString(TypedArray a, int index, String value) {
684        // If <case> does not have "index" attribute, that means this <case> is wild-card for the
685        // attribute.
686        return !a.hasValue(index) || stringArrayContains(a.getString(index).split("\\|"), value);
687    }
688
689    private static boolean matchTypedValue(TypedArray a, int index, int intValue, String strValue) {
690        // If <case> does not have "index" attribute, that means this <case> is wild-card for the
691        // attribute.
692        final TypedValue v = a.peekValue(index);
693        if (v == null)
694            return true;
695
696        if (isIntegerValue(v)) {
697            return intValue == a.getInt(index, 0);
698        } else if (isStringValue(v)) {
699            return stringArrayContains(a.getString(index).split("\\|"), strValue);
700        }
701        return false;
702    }
703
704    private static boolean stringArrayContains(String[] array, String value) {
705        for (final String elem : array) {
706            if (elem.equals(value))
707                return true;
708        }
709        return false;
710    }
711
712    private boolean parseDefault(XmlPullParser parser, Row row, boolean skip)
713            throws XmlPullParserException, IOException {
714        if (DEBUG) Log.d(TAG, String.format("<%s>", TAG_DEFAULT));
715        if (row == null) {
716            parseKeyboardContent(parser, skip);
717        } else {
718            parseRowContent(parser, row, skip);
719        }
720        return true;
721    }
722
723    private void parseKeyStyle(XmlPullParser parser, boolean skip)
724            throws XmlPullParserException {
725        TypedArray keyStyleAttr = mResources.obtainAttributes(Xml.asAttributeSet(parser),
726                R.styleable.Keyboard_KeyStyle);
727        TypedArray keyAttrs = mResources.obtainAttributes(Xml.asAttributeSet(parser),
728                R.styleable.Keyboard_Key);
729        try {
730            if (!keyStyleAttr.hasValue(R.styleable.Keyboard_KeyStyle_styleName))
731                throw new XmlParseUtils.ParseException("<" + TAG_KEY_STYLE
732                        + "/> needs styleName attribute", parser);
733            if (!skip)
734                mKeyStyles.parseKeyStyleAttributes(keyStyleAttr, keyAttrs, parser);
735        } finally {
736            keyStyleAttr.recycle();
737            keyAttrs.recycle();
738        }
739    }
740
741    private void startKeyboard() {
742        mCurrentY += mParams.mTopPadding;
743        mTopEdge = true;
744    }
745
746    private void startRow(Row row) {
747        addEdgeSpace(mParams.mHorizontalEdgesPadding, row);
748        mCurrentRow = row;
749        mLeftEdge = true;
750        mRightEdgeKey = null;
751    }
752
753    private void endRow(Row row) {
754        if (mCurrentRow == null)
755            throw new InflateException("orphant end row tag");
756        if (mRightEdgeKey != null) {
757            mRightEdgeKey.markAsRightEdge(mParams);
758            mRightEdgeKey = null;
759        }
760        addEdgeSpace(mParams.mHorizontalEdgesPadding, row);
761        mCurrentY += row.mRowHeight;
762        mCurrentRow = null;
763        mTopEdge = false;
764    }
765
766    private void endKey(Key key) {
767        mParams.onAddKey(key);
768        if (mLeftEdge) {
769            key.markAsLeftEdge(mParams);
770            mLeftEdge = false;
771        }
772        if (mTopEdge) {
773            key.markAsTopEdge(mParams);
774        }
775        mRightEdgeKey = key;
776    }
777
778    private void endKeyboard() {
779        // nothing to do here.
780    }
781
782    private void addEdgeSpace(float width, Row row) {
783        row.advanceXPos(width);
784        mLeftEdge = false;
785        mRightEdgeKey = null;
786    }
787
788    public static float getDimensionOrFraction(TypedArray a, int index, int base, float defValue) {
789        final TypedValue value = a.peekValue(index);
790        if (value == null)
791            return defValue;
792        if (isFractionValue(value)) {
793            return a.getFraction(index, base, base, defValue);
794        } else if (isDimensionValue(value)) {
795            return a.getDimension(index, defValue);
796        }
797        return defValue;
798    }
799
800    public static int getEnumValue(TypedArray a, int index, int defValue) {
801        final TypedValue value = a.peekValue(index);
802        if (value == null)
803            return defValue;
804        if (isIntegerValue(value)) {
805            return a.getInt(index, defValue);
806        }
807        return defValue;
808    }
809
810    private static boolean isFractionValue(TypedValue v) {
811        return v.type == TypedValue.TYPE_FRACTION;
812    }
813
814    private static boolean isDimensionValue(TypedValue v) {
815        return v.type == TypedValue.TYPE_DIMENSION;
816    }
817
818    private static boolean isIntegerValue(TypedValue v) {
819        return v.type >= TypedValue.TYPE_FIRST_INT && v.type <= TypedValue.TYPE_LAST_INT;
820    }
821
822    private static boolean isStringValue(TypedValue v) {
823        return v.type == TypedValue.TYPE_STRING;
824    }
825
826    private static String textAttr(String value, String name) {
827        return value != null ? String.format(" %s=%s", name, value) : "";
828    }
829
830    private static String booleanAttr(TypedArray a, int index, String name) {
831        return a.hasValue(index) ? String.format(" %s=%s", name, a.getBoolean(index, false)) : "";
832    }
833}
834