Keyboard.java revision 411749a4baddc0fda7720deb6e4d67c1f1558cc5
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;
18
19import android.content.Context;
20import android.content.res.Resources;
21import android.content.res.TypedArray;
22import android.content.res.XmlResourceParser;
23import android.text.TextUtils;
24import android.util.AttributeSet;
25import android.util.DisplayMetrics;
26import android.util.Log;
27import android.util.TypedValue;
28import android.util.Xml;
29import android.view.InflateException;
30
31import com.android.inputmethod.keyboard.internal.KeyStyles;
32import com.android.inputmethod.keyboard.internal.KeyboardIconsSet;
33import com.android.inputmethod.latin.LatinImeLogger;
34import com.android.inputmethod.latin.R;
35import com.android.inputmethod.latin.XmlParseUtils;
36
37import org.xmlpull.v1.XmlPullParser;
38import org.xmlpull.v1.XmlPullParserException;
39
40import java.io.IOException;
41import java.util.ArrayList;
42import java.util.Arrays;
43import java.util.Collections;
44import java.util.HashMap;
45import java.util.HashSet;
46import java.util.List;
47import java.util.Map;
48import java.util.Set;
49
50/**
51 * Loads an XML description of a keyboard and stores the attributes of the keys. A keyboard
52 * consists of rows of keys.
53 * <p>The layout file for a keyboard contains XML that looks like the following snippet:</p>
54 * <pre>
55 * &lt;Keyboard
56 *         latin:keyWidth="%10p"
57 *         latin:keyHeight="50px"
58 *         latin:horizontalGap="2px"
59 *         latin:verticalGap="2px" &gt;
60 *     &lt;Row latin:keyWidth="32px" &gt;
61 *         &lt;Key latin:keyLabel="A" /&gt;
62 *         ...
63 *     &lt;/Row&gt;
64 *     ...
65 * &lt;/Keyboard&gt;
66 * </pre>
67 */
68public class Keyboard {
69    private static final String TAG = Keyboard.class.getSimpleName();
70
71    /** Some common keys code. Must be positive.
72     * These should be aligned with values/keycodes.xml
73     */
74    public static final int CODE_ENTER = '\n';
75    public static final int CODE_TAB = '\t';
76    public static final int CODE_SPACE = ' ';
77    public static final int CODE_PERIOD = '.';
78    public static final int CODE_DASH = '-';
79    public static final int CODE_SINGLE_QUOTE = '\'';
80    public static final int CODE_DOUBLE_QUOTE = '"';
81    // TODO: Check how this should work for right-to-left languages. It seems to stand
82    // that for rtl languages, a closing parenthesis is a left parenthesis. Is this
83    // managed by the font? Or is it a different char?
84    public static final int CODE_CLOSING_PARENTHESIS = ')';
85    public static final int CODE_CLOSING_SQUARE_BRACKET = ']';
86    public static final int CODE_CLOSING_CURLY_BRACKET = '}';
87    public static final int CODE_CLOSING_ANGLE_BRACKET = '>';
88    private static final int MINIMUM_LETTER_CODE = CODE_TAB;
89
90    /** Special keys code. Must be negative.
91     * These should be aligned with values/keycodes.xml
92     */
93    public static final int CODE_SHIFT = -1;
94    public static final int CODE_SWITCH_ALPHA_SYMBOL = -2;
95    public static final int CODE_OUTPUT_TEXT = -3;
96    public static final int CODE_DELETE = -4;
97    public static final int CODE_SETTINGS = -5;
98    public static final int CODE_SHORTCUT = -6;
99    public static final int CODE_ACTION_ENTER = -7;
100    public static final int CODE_ACTION_NEXT = -8;
101    public static final int CODE_ACTION_PREVIOUS = -9;
102    public static final int CODE_LANGUAGE_SWITCH = -10;
103    // Code value representing the code is not specified.
104    public static final int CODE_UNSPECIFIED = -11;
105
106    public final KeyboardId mId;
107    public final int mThemeId;
108
109    /** Total height of the keyboard, including the padding and keys */
110    public final int mOccupiedHeight;
111    /** Total width of the keyboard, including the padding and keys */
112    public final int mOccupiedWidth;
113
114    /** The padding above the keyboard */
115    public final int mTopPadding;
116    /** Default gap between rows */
117    public final int mVerticalGap;
118
119    public final int mMostCommonKeyHeight;
120    public final int mMostCommonKeyWidth;
121
122    /** More keys keyboard template */
123    public final int mMoreKeysTemplate;
124
125    /** Maximum column for more keys keyboard */
126    public final int mMaxMoreKeysKeyboardColumn;
127
128    /** List of keys and icons in this keyboard */
129    public final Set<Key> mKeys;
130    public final Set<Key> mShiftKeys;
131    public final KeyboardIconsSet mIconsSet;
132
133    private final Map<Integer, Key> mKeyCache = new HashMap<Integer, Key>();
134
135    private final ProximityInfo mProximityInfo;
136
137    public final Map<Integer, List<Integer>> mAdditionalProximityChars;
138
139    public Keyboard(Params params) {
140        mId = params.mId;
141        mThemeId = params.mThemeId;
142        mOccupiedHeight = params.mOccupiedHeight;
143        mOccupiedWidth = params.mOccupiedWidth;
144        mMostCommonKeyHeight = params.mMostCommonKeyHeight;
145        mMostCommonKeyWidth = params.mMostCommonKeyWidth;
146        mMoreKeysTemplate = params.mMoreKeysTemplate;
147        mMaxMoreKeysKeyboardColumn = params.mMaxMoreKeysKeyboardColumn;
148
149        mTopPadding = params.mTopPadding;
150        mVerticalGap = params.mVerticalGap;
151
152        mKeys = Collections.unmodifiableSet(params.mKeys);
153        mShiftKeys = Collections.unmodifiableSet(params.mShiftKeys);
154        mIconsSet = params.mIconsSet;
155        mAdditionalProximityChars = params.mAdditionalProximityChars;
156
157        mProximityInfo = new ProximityInfo(
158                params.GRID_WIDTH, params.GRID_HEIGHT, mOccupiedWidth, mOccupiedHeight,
159                mMostCommonKeyWidth, mMostCommonKeyHeight, mKeys, params.mTouchPositionCorrection,
160                params.mAdditionalProximityChars);
161    }
162
163    public ProximityInfo getProximityInfo() {
164        return mProximityInfo;
165    }
166
167    public Key getKey(int code) {
168        if (code == CODE_UNSPECIFIED) {
169            return null;
170        }
171        final Integer keyCode = code;
172        if (mKeyCache.containsKey(keyCode)) {
173            return mKeyCache.get(keyCode);
174        }
175
176        for (final Key key : mKeys) {
177            if (key.mCode == code) {
178                mKeyCache.put(keyCode, key);
179                return key;
180            }
181        }
182        mKeyCache.put(keyCode, null);
183        return null;
184    }
185
186    // TODO: Remove this method.
187    public boolean isShiftedOrShiftLocked() {
188        // Alphabet mode have unshifted, manual shifted, automatic shifted, shift locked, and
189        // shift lock shifted element. So that unshifed element is the only one that is NOT in
190        // shifted or shift locked state.
191        return mId.isAlphabetKeyboard() && mId.mElementId != KeyboardId.ELEMENT_ALPHABET;
192    }
193
194    public static boolean isLetterCode(int code) {
195        return code >= MINIMUM_LETTER_CODE;
196    }
197
198    public static class Params {
199        public KeyboardId mId;
200        public int mThemeId;
201
202        /** Total height and width of the keyboard, including the paddings and keys */
203        public int mOccupiedHeight;
204        public int mOccupiedWidth;
205
206        /** Base height and width of the keyboard used to calculate rows' or keys' heights and
207         *  widths
208         */
209        public int mBaseHeight;
210        public int mBaseWidth;
211
212        public int mTopPadding;
213        public int mBottomPadding;
214        public int mHorizontalEdgesPadding;
215        public int mHorizontalCenterPadding;
216
217        public int mDefaultRowHeight;
218        public int mDefaultKeyWidth;
219        public int mHorizontalGap;
220        public int mVerticalGap;
221
222        public int mMoreKeysTemplate;
223        public int mMaxMoreKeysKeyboardColumn;
224
225        public int GRID_WIDTH;
226        public int GRID_HEIGHT;
227
228        public final Set<Key> mKeys = new HashSet<Key>();
229        public final Set<Key> mShiftKeys = new HashSet<Key>();
230        public final KeyboardIconsSet mIconsSet = new KeyboardIconsSet();
231        // TODO: Should be in Key instead of Keyboard.Params?
232        public final Map<Integer, List<Integer>> mAdditionalProximityChars =
233                new HashMap<Integer, List<Integer>>();
234
235        public KeyboardSet.KeysCache mKeysCache;
236
237        public int mMostCommonKeyHeight = 0;
238        public int mMostCommonKeyWidth = 0;
239
240        public final TouchPositionCorrection mTouchPositionCorrection =
241                new TouchPositionCorrection();
242
243        public static class TouchPositionCorrection {
244            private static final int TOUCH_POSITION_CORRECTION_RECORD_SIZE = 3;
245
246            public boolean mEnabled;
247            public float[] mXs;
248            public float[] mYs;
249            public float[] mRadii;
250
251            public void load(String[] data) {
252                final int dataLength = data.length;
253                if (dataLength % TOUCH_POSITION_CORRECTION_RECORD_SIZE != 0) {
254                    if (LatinImeLogger.sDBG)
255                        throw new RuntimeException(
256                                "the size of touch position correction data is invalid");
257                    return;
258                }
259
260                final int length = dataLength / TOUCH_POSITION_CORRECTION_RECORD_SIZE;
261                mXs = new float[length];
262                mYs = new float[length];
263                mRadii = new float[length];
264                try {
265                    for (int i = 0; i < dataLength; ++i) {
266                        final int type = i % TOUCH_POSITION_CORRECTION_RECORD_SIZE;
267                        final int index = i / TOUCH_POSITION_CORRECTION_RECORD_SIZE;
268                        final float value = Float.parseFloat(data[i]);
269                        if (type == 0) {
270                            mXs[index] = value;
271                        } else if (type == 1) {
272                            mYs[index] = value;
273                        } else {
274                            mRadii[index] = value;
275                        }
276                    }
277                } catch (NumberFormatException e) {
278                    if (LatinImeLogger.sDBG) {
279                        throw new RuntimeException(
280                                "the number format for touch position correction data is invalid");
281                    }
282                    mXs = null;
283                    mYs = null;
284                    mRadii = null;
285                }
286            }
287
288            public void setEnabled(boolean enabled) {
289                mEnabled = enabled;
290            }
291
292            public boolean isValid() {
293                return mEnabled && mXs != null && mYs != null && mRadii != null
294                    && mXs.length > 0 && mYs.length > 0 && mRadii.length > 0;
295            }
296        }
297
298        protected void clearKeys() {
299            mKeys.clear();
300            mShiftKeys.clear();
301            clearHistogram();
302        }
303
304        public void onAddKey(Key newKey) {
305            final Key key = (mKeysCache != null) ? mKeysCache.get(newKey) : newKey;
306            mKeys.add(key);
307            updateHistogram(key);
308            if (key.mCode == Keyboard.CODE_SHIFT) {
309                mShiftKeys.add(key);
310            }
311        }
312
313        private int mMaxHeightCount = 0;
314        private int mMaxWidthCount = 0;
315        private final Map<Integer, Integer> mHeightHistogram = new HashMap<Integer, Integer>();
316        private final Map<Integer, Integer> mWidthHistogram = new HashMap<Integer, Integer>();
317
318        private void clearHistogram() {
319            mMostCommonKeyHeight = 0;
320            mMaxHeightCount = 0;
321            mHeightHistogram.clear();
322
323            mMaxWidthCount = 0;
324            mMostCommonKeyWidth = 0;
325            mWidthHistogram.clear();
326        }
327
328        private static int updateHistogramCounter(Map<Integer, Integer> histogram, Integer key) {
329            final int count = (histogram.containsKey(key) ? histogram.get(key) : 0) + 1;
330            histogram.put(key, count);
331            return count;
332        }
333
334        private void updateHistogram(Key key) {
335            final Integer height = key.mHeight + key.mVerticalGap;
336            final int heightCount = updateHistogramCounter(mHeightHistogram, height);
337            if (heightCount > mMaxHeightCount) {
338                mMaxHeightCount = heightCount;
339                mMostCommonKeyHeight = height;
340            }
341
342            final Integer width = key.mWidth + key.mHorizontalGap;
343            final int widthCount = updateHistogramCounter(mWidthHistogram, width);
344            if (widthCount > mMaxWidthCount) {
345                mMaxWidthCount = widthCount;
346                mMostCommonKeyWidth = width;
347            }
348        }
349    }
350
351    /**
352     * Returns the array of the keys that are closest to the given point.
353     * @param x the x-coordinate of the point
354     * @param y the y-coordinate of the point
355     * @return the array of the nearest keys to the given point. If the given
356     * point is out of range, then an array of size zero is returned.
357     */
358    public Key[] getNearestKeys(int x, int y) {
359        // Avoid dead pixels at edges of the keyboard
360        final int adjustedX = Math.max(0, Math.min(x, mOccupiedWidth - 1));
361        final int adjustedY = Math.max(0, Math.min(y, mOccupiedHeight - 1));
362        return mProximityInfo.getNearestKeys(adjustedX, adjustedY);
363    }
364
365    public Map<Integer, List<Integer>> getAdditionalProximityChars() {
366        return mAdditionalProximityChars;
367    }
368
369    public static String printableCode(int code) {
370        switch (code) {
371        case CODE_SHIFT: return "shift";
372        case CODE_SWITCH_ALPHA_SYMBOL: return "symbol";
373        case CODE_OUTPUT_TEXT: return "text";
374        case CODE_DELETE: return "delete";
375        case CODE_SETTINGS: return "settings";
376        case CODE_SHORTCUT: return "shortcut";
377        case CODE_ACTION_ENTER: return "actionEnter";
378        case CODE_ACTION_NEXT: return "actionNext";
379        case CODE_ACTION_PREVIOUS: return "actionPrevious";
380        case CODE_UNSPECIFIED: return "unspec";
381        case CODE_TAB: return "tab";
382        case CODE_ENTER: return "enter";
383        default:
384            if (code <= 0) Log.w(TAG, "Unknown non-positive key code=" + code);
385            if (code < CODE_SPACE) return String.format("'\\u%02x'", code);
386            if (code < 0x100) return String.format("'%c'", code);
387            return String.format("'\\u%04x'", code);
388        }
389    }
390
391   /**
392     * Keyboard Building helper.
393     *
394     * This class parses Keyboard XML file and eventually build a Keyboard.
395     * The Keyboard XML file looks like:
396     * <pre>
397     *   &gt;!-- xml/keyboard.xml --&lt;
398     *   &gt;Keyboard keyboard_attributes*&lt;
399     *     &gt;!-- Keyboard Content --&lt;
400     *     &gt;Row row_attributes*&lt;
401     *       &gt;!-- Row Content --&lt;
402     *       &gt;Key key_attributes* /&lt;
403     *       &gt;Spacer horizontalGap="0.2in" /&lt;
404     *       &gt;include keyboardLayout="@xml/other_keys"&lt;
405     *       ...
406     *     &gt;/Row&lt;
407     *     &gt;include keyboardLayout="@xml/other_rows"&lt;
408     *     ...
409     *   &gt;/Keyboard&lt;
410     * </pre>
411     * The XML file which is included in other file must have &gt;merge&lt; as root element,
412     * such as:
413     * <pre>
414     *   &gt;!-- xml/other_keys.xml --&lt;
415     *   &gt;merge&lt;
416     *     &gt;Key key_attributes* /&lt;
417     *     ...
418     *   &gt;/merge&lt;
419     * </pre>
420     * and
421     * <pre>
422     *   &gt;!-- xml/other_rows.xml --&lt;
423     *   &gt;merge&lt;
424     *     &gt;Row row_attributes*&lt;
425     *       &gt;Key key_attributes* /&lt;
426     *     &gt;/Row&lt;
427     *     ...
428     *   &gt;/merge&lt;
429     * </pre>
430     * You can also use switch-case-default tags to select Rows and Keys.
431     * <pre>
432     *   &gt;switch&lt;
433     *     &gt;case case_attribute*&lt;
434     *       &gt;!-- Any valid tags at switch position --&lt;
435     *     &gt;/case&lt;
436     *     ...
437     *     &gt;default&lt;
438     *       &gt;!-- Any valid tags at switch position --&lt;
439     *     &gt;/default&lt;
440     *   &gt;/switch&lt;
441     * </pre>
442     * You can declare Key style and specify styles within Key tags.
443     * <pre>
444     *     &gt;switch&lt;
445     *       &gt;case mode="email"&lt;
446     *         &gt;key-style styleName="f1-key" parentStyle="modifier-key"
447     *           keyLabel=".com"
448     *         /&lt;
449     *       &gt;/case&lt;
450     *       &gt;case mode="url"&lt;
451     *         &gt;key-style styleName="f1-key" parentStyle="modifier-key"
452     *           keyLabel="http://"
453     *         /&lt;
454     *       &gt;/case&lt;
455     *     &gt;/switch&lt;
456     *     ...
457     *     &gt;Key keyStyle="shift-key" ... /&lt;
458     * </pre>
459     */
460
461    public static class Builder<KP extends Params> {
462        private static final String BUILDER_TAG = "Keyboard.Builder";
463        private static final boolean DEBUG = false;
464
465        // Keyboard XML Tags
466        private static final String TAG_KEYBOARD = "Keyboard";
467        private static final String TAG_ROW = "Row";
468        private static final String TAG_KEY = "Key";
469        private static final String TAG_SPACER = "Spacer";
470        private static final String TAG_INCLUDE = "include";
471        private static final String TAG_MERGE = "merge";
472        private static final String TAG_SWITCH = "switch";
473        private static final String TAG_CASE = "case";
474        private static final String TAG_DEFAULT = "default";
475        public static final String TAG_KEY_STYLE = "key-style";
476
477        private static final int DEFAULT_KEYBOARD_COLUMNS = 10;
478        private static final int DEFAULT_KEYBOARD_ROWS = 4;
479
480        protected final KP mParams;
481        protected final Context mContext;
482        protected final Resources mResources;
483        private final DisplayMetrics mDisplayMetrics;
484
485        private int mCurrentY = 0;
486        private Row mCurrentRow = null;
487        private boolean mLeftEdge;
488        private boolean mTopEdge;
489        private Key mRightEdgeKey = null;
490        private final KeyStyles mKeyStyles = new KeyStyles();
491
492        /**
493         * Container for keys in the keyboard. All keys in a row are at the same Y-coordinate.
494         * Some of the key size defaults can be overridden per row from what the {@link Keyboard}
495         * defines.
496         */
497        public static class Row {
498            // keyWidth enum constants
499            private static final int KEYWIDTH_NOT_ENUM = 0;
500            private static final int KEYWIDTH_FILL_RIGHT = -1;
501            private static final int KEYWIDTH_FILL_BOTH = -2;
502
503            private final Params mParams;
504            /** Default width of a key in this row. */
505            private float mDefaultKeyWidth;
506            /** Default height of a key in this row. */
507            public final int mRowHeight;
508
509            private final int mCurrentY;
510            // Will be updated by {@link Key}'s constructor.
511            private float mCurrentX;
512
513            public Row(Resources res, Params params, XmlPullParser parser, int y) {
514                mParams = params;
515                TypedArray keyboardAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
516                        R.styleable.Keyboard);
517                mRowHeight = (int)Builder.getDimensionOrFraction(keyboardAttr,
518                        R.styleable.Keyboard_rowHeight,
519                        params.mBaseHeight, params.mDefaultRowHeight);
520                keyboardAttr.recycle();
521                TypedArray keyAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
522                        R.styleable.Keyboard_Key);
523                mDefaultKeyWidth = Builder.getDimensionOrFraction(keyAttr,
524                        R.styleable.Keyboard_Key_keyWidth,
525                        params.mBaseWidth, params.mDefaultKeyWidth);
526                keyAttr.recycle();
527
528                mCurrentY = y;
529                mCurrentX = 0.0f;
530            }
531
532            public float getDefaultKeyWidth() {
533                return mDefaultKeyWidth;
534            }
535
536            public void setDefaultKeyWidth(float defaultKeyWidth) {
537                mDefaultKeyWidth = defaultKeyWidth;
538            }
539
540            public void setXPos(float keyXPos) {
541                mCurrentX = keyXPos;
542            }
543
544            public void advanceXPos(float width) {
545                mCurrentX += width;
546            }
547
548            public int getKeyY() {
549                return mCurrentY;
550            }
551
552            public float getKeyX(TypedArray keyAttr) {
553                final int widthType = Builder.getEnumValue(keyAttr,
554                        R.styleable.Keyboard_Key_keyWidth, KEYWIDTH_NOT_ENUM);
555                if (widthType == KEYWIDTH_FILL_BOTH) {
556                    // If keyWidth is fillBoth, the key width should start right after the nearest
557                    // key on the left hand side.
558                    return mCurrentX;
559                }
560
561                final int keyboardRightEdge = mParams.mOccupiedWidth
562                        - mParams.mHorizontalEdgesPadding;
563                if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyXPos)) {
564                    final float keyXPos = Builder.getDimensionOrFraction(keyAttr,
565                            R.styleable.Keyboard_Key_keyXPos, mParams.mBaseWidth, 0);
566                    if (keyXPos < 0) {
567                        // If keyXPos is negative, the actual x-coordinate will be
568                        // keyboardWidth + keyXPos.
569                        // keyXPos shouldn't be less than mCurrentX because drawable area for this
570                        // key starts at mCurrentX. Or, this key will overlaps the adjacent key on
571                        // its left hand side.
572                        return Math.max(keyXPos + keyboardRightEdge, mCurrentX);
573                    } else {
574                        return keyXPos + mParams.mHorizontalEdgesPadding;
575                    }
576                }
577                return mCurrentX;
578            }
579
580            public float getKeyWidth(TypedArray keyAttr) {
581                return getKeyWidth(keyAttr, mCurrentX);
582            }
583
584            public float getKeyWidth(TypedArray keyAttr, float keyXPos) {
585                final int widthType = Builder.getEnumValue(keyAttr,
586                        R.styleable.Keyboard_Key_keyWidth, KEYWIDTH_NOT_ENUM);
587                switch (widthType) {
588                case KEYWIDTH_FILL_RIGHT:
589                case KEYWIDTH_FILL_BOTH:
590                    final int keyboardRightEdge =
591                            mParams.mOccupiedWidth - mParams.mHorizontalEdgesPadding;
592                    // If keyWidth is fillRight, the actual key width will be determined to fill
593                    // out the area up to the right edge of the keyboard.
594                    // If keyWidth is fillBoth, the actual key width will be determined to fill out
595                    // the area between the nearest key on the left hand side and the right edge of
596                    // the keyboard.
597                    return keyboardRightEdge - keyXPos;
598                default: // KEYWIDTH_NOT_ENUM
599                    return Builder.getDimensionOrFraction(keyAttr,
600                            R.styleable.Keyboard_Key_keyWidth,
601                            mParams.mBaseWidth, mDefaultKeyWidth);
602                }
603            }
604        }
605
606        public Builder(Context context, KP params) {
607            mContext = context;
608            final Resources res = context.getResources();
609            mResources = res;
610            mDisplayMetrics = res.getDisplayMetrics();
611
612            mParams = params;
613
614            setTouchPositionCorrectionData(context, params);
615            setAdditionalProximityChars(context, params);
616
617            params.GRID_WIDTH = res.getInteger(R.integer.config_keyboard_grid_width);
618            params.GRID_HEIGHT = res.getInteger(R.integer.config_keyboard_grid_height);
619        }
620
621        private static void setTouchPositionCorrectionData(Context context, Params params) {
622            final TypedArray a = context.obtainStyledAttributes(
623                    null, R.styleable.Keyboard, R.attr.keyboardStyle, 0);
624            params.mThemeId = a.getInt(R.styleable.Keyboard_themeId, 0);
625            final int resourceId = a.getResourceId(
626                    R.styleable.Keyboard_touchPositionCorrectionData, 0);
627            a.recycle();
628            if (resourceId == 0) {
629                if (LatinImeLogger.sDBG)
630                    Log.e(BUILDER_TAG, "touchPositionCorrectionData is not defined");
631                return;
632            }
633
634            final String[] data = context.getResources().getStringArray(resourceId);
635            params.mTouchPositionCorrection.load(data);
636        }
637
638        private static void setAdditionalProximityChars(Context context, Params params) {
639            final String[] additionalChars =
640                    context.getResources().getStringArray(R.array.additional_proximitychars);
641            int currentPrimaryIndex = 0;
642            for (int i = 0; i < additionalChars.length; ++i) {
643                final String additionalChar = additionalChars[i];
644                if (TextUtils.isEmpty(additionalChar)) {
645                    currentPrimaryIndex = 0;
646                } else if (currentPrimaryIndex == 0) {
647                    currentPrimaryIndex = additionalChar.charAt(0);
648                    params.mAdditionalProximityChars.put(
649                            currentPrimaryIndex, new ArrayList<Integer>());
650                } else if (currentPrimaryIndex != 0) {
651                    final int c = additionalChar.charAt(0);
652                    params.mAdditionalProximityChars.get(currentPrimaryIndex).add(c);
653                }
654            }
655        }
656
657        public void setAutoGenerate(KeyboardSet.KeysCache keysCache) {
658            mParams.mKeysCache = keysCache;
659        }
660
661        public Builder<KP> load(int xmlId, KeyboardId id) {
662            mParams.mId = id;
663            final XmlResourceParser parser = mResources.getXml(xmlId);
664            try {
665                parseKeyboard(parser);
666            } catch (XmlPullParserException e) {
667                Log.w(BUILDER_TAG, "keyboard XML parse error: " + e);
668                throw new IllegalArgumentException(e);
669            } catch (IOException e) {
670                Log.w(BUILDER_TAG, "keyboard XML parse error: " + e);
671                throw new RuntimeException(e);
672            } finally {
673                parser.close();
674            }
675            return this;
676        }
677
678        public void setTouchPositionCorrectionEnabled(boolean enabled) {
679            mParams.mTouchPositionCorrection.setEnabled(enabled);
680        }
681
682        public Keyboard build() {
683            return new Keyboard(mParams);
684        }
685
686        private int mIndent;
687        private static final String SPACES = "                                             ";
688
689        private static String spaces(int count) {
690            return (count < SPACES.length()) ? SPACES.substring(0, count) : SPACES;
691        }
692
693        private void startTag(String format, Object ... args) {
694            Log.d(BUILDER_TAG, String.format(spaces(++mIndent * 2) + format, args));
695        }
696
697        private void endTag(String format, Object ... args) {
698            Log.d(BUILDER_TAG, String.format(spaces(mIndent-- * 2) + format, args));
699        }
700
701        private void startEndTag(String format, Object ... args) {
702            Log.d(BUILDER_TAG, String.format(spaces(++mIndent * 2) + format, args));
703            mIndent--;
704        }
705
706        private void parseKeyboard(XmlPullParser parser)
707                throws XmlPullParserException, IOException {
708            if (DEBUG) startTag("<%s> %s", TAG_KEYBOARD, mParams.mId);
709            int event;
710            while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
711                if (event == XmlPullParser.START_TAG) {
712                    final String tag = parser.getName();
713                    if (TAG_KEYBOARD.equals(tag)) {
714                        parseKeyboardAttributes(parser);
715                        startKeyboard();
716                        parseKeyboardContent(parser, false);
717                        break;
718                    } else {
719                        throw new XmlParseUtils.IllegalStartTag(parser, TAG_KEYBOARD);
720                    }
721                }
722            }
723        }
724
725        private void parseKeyboardAttributes(XmlPullParser parser) {
726            final int displayWidth = mDisplayMetrics.widthPixels;
727            final TypedArray keyboardAttr = mContext.obtainStyledAttributes(
728                    Xml.asAttributeSet(parser), R.styleable.Keyboard, R.attr.keyboardStyle,
729                    R.style.Keyboard);
730            final TypedArray keyAttr = mResources.obtainAttributes(Xml.asAttributeSet(parser),
731                    R.styleable.Keyboard_Key);
732            try {
733                final int displayHeight = mDisplayMetrics.heightPixels;
734                final int keyboardHeight = (int)keyboardAttr.getDimension(
735                        R.styleable.Keyboard_keyboardHeight, displayHeight / 2);
736                final int maxKeyboardHeight = (int)getDimensionOrFraction(keyboardAttr,
737                        R.styleable.Keyboard_maxKeyboardHeight, displayHeight, displayHeight / 2);
738                int minKeyboardHeight = (int)getDimensionOrFraction(keyboardAttr,
739                        R.styleable.Keyboard_minKeyboardHeight, displayHeight, displayHeight / 2);
740                if (minKeyboardHeight < 0) {
741                    // Specified fraction was negative, so it should be calculated against display
742                    // width.
743                    minKeyboardHeight = -(int)getDimensionOrFraction(keyboardAttr,
744                            R.styleable.Keyboard_minKeyboardHeight, displayWidth, displayWidth / 2);
745                }
746                final Params params = mParams;
747                // Keyboard height will not exceed maxKeyboardHeight and will not be less than
748                // minKeyboardHeight.
749                params.mOccupiedHeight = Math.max(
750                        Math.min(keyboardHeight, maxKeyboardHeight), minKeyboardHeight);
751                params.mOccupiedWidth = params.mId.mWidth;
752                params.mTopPadding = (int)getDimensionOrFraction(keyboardAttr,
753                        R.styleable.Keyboard_keyboardTopPadding, params.mOccupiedHeight, 0);
754                params.mBottomPadding = (int)getDimensionOrFraction(keyboardAttr,
755                        R.styleable.Keyboard_keyboardBottomPadding, params.mOccupiedHeight, 0);
756                params.mHorizontalEdgesPadding = (int)getDimensionOrFraction(keyboardAttr,
757                        R.styleable.Keyboard_keyboardHorizontalEdgesPadding,
758                        mParams.mOccupiedWidth, 0);
759
760                params.mBaseWidth = params.mOccupiedWidth - params.mHorizontalEdgesPadding * 2
761                        - params.mHorizontalCenterPadding;
762                params.mDefaultKeyWidth = (int)getDimensionOrFraction(keyAttr,
763                        R.styleable.Keyboard_Key_keyWidth, params.mBaseWidth,
764                        params.mBaseWidth / DEFAULT_KEYBOARD_COLUMNS);
765                params.mHorizontalGap = (int)getDimensionOrFraction(keyboardAttr,
766                        R.styleable.Keyboard_horizontalGap, params.mBaseWidth, 0);
767                params.mVerticalGap = (int)getDimensionOrFraction(keyboardAttr,
768                        R.styleable.Keyboard_verticalGap, params.mOccupiedHeight, 0);
769                params.mBaseHeight = params.mOccupiedHeight - params.mTopPadding
770                        - params.mBottomPadding + params.mVerticalGap;
771                params.mDefaultRowHeight = (int)getDimensionOrFraction(keyboardAttr,
772                        R.styleable.Keyboard_rowHeight, params.mBaseHeight,
773                        params.mBaseHeight / DEFAULT_KEYBOARD_ROWS);
774
775                params.mMoreKeysTemplate = keyboardAttr.getResourceId(
776                        R.styleable.Keyboard_moreKeysTemplate, 0);
777                params.mMaxMoreKeysKeyboardColumn = keyAttr.getInt(
778                        R.styleable.Keyboard_Key_maxMoreKeysColumn, 5);
779
780                params.mIconsSet.loadIcons(keyboardAttr);
781            } finally {
782                keyAttr.recycle();
783                keyboardAttr.recycle();
784            }
785        }
786
787        private void parseKeyboardContent(XmlPullParser parser, boolean skip)
788                throws XmlPullParserException, IOException {
789            int event;
790            while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
791                if (event == XmlPullParser.START_TAG) {
792                    final String tag = parser.getName();
793                    if (TAG_ROW.equals(tag)) {
794                        Row row = parseRowAttributes(parser);
795                        if (DEBUG) startTag("<%s>%s", TAG_ROW, skip ? " skipped" : "");
796                        if (!skip) {
797                            startRow(row);
798                        }
799                        parseRowContent(parser, row, skip);
800                    } else if (TAG_INCLUDE.equals(tag)) {
801                        parseIncludeKeyboardContent(parser, skip);
802                    } else if (TAG_SWITCH.equals(tag)) {
803                        parseSwitchKeyboardContent(parser, skip);
804                    } else if (TAG_KEY_STYLE.equals(tag)) {
805                        parseKeyStyle(parser, skip);
806                    } else {
807                        throw new XmlParseUtils.IllegalStartTag(parser, TAG_ROW);
808                    }
809                } else if (event == XmlPullParser.END_TAG) {
810                    final String tag = parser.getName();
811                    if (DEBUG) endTag("</%s>", tag);
812                    if (TAG_KEYBOARD.equals(tag)) {
813                        endKeyboard();
814                        break;
815                    } else if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag)
816                            || TAG_MERGE.equals(tag)) {
817                        break;
818                    } else {
819                        throw new XmlParseUtils.IllegalEndTag(parser, TAG_ROW);
820                    }
821                }
822            }
823        }
824
825        private Row parseRowAttributes(XmlPullParser parser) throws XmlPullParserException {
826            final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
827                    R.styleable.Keyboard);
828            try {
829                if (a.hasValue(R.styleable.Keyboard_horizontalGap))
830                    throw new XmlParseUtils.IllegalAttribute(parser, "horizontalGap");
831                if (a.hasValue(R.styleable.Keyboard_verticalGap))
832                    throw new XmlParseUtils.IllegalAttribute(parser, "verticalGap");
833                return new Row(mResources, mParams, parser, mCurrentY);
834            } finally {
835                a.recycle();
836            }
837        }
838
839        private void parseRowContent(XmlPullParser parser, Row row, boolean skip)
840                throws XmlPullParserException, IOException {
841            int event;
842            while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
843                if (event == XmlPullParser.START_TAG) {
844                    final String tag = parser.getName();
845                    if (TAG_KEY.equals(tag)) {
846                        parseKey(parser, row, skip);
847                    } else if (TAG_SPACER.equals(tag)) {
848                        parseSpacer(parser, row, skip);
849                    } else if (TAG_INCLUDE.equals(tag)) {
850                        parseIncludeRowContent(parser, row, skip);
851                    } else if (TAG_SWITCH.equals(tag)) {
852                        parseSwitchRowContent(parser, row, skip);
853                    } else if (TAG_KEY_STYLE.equals(tag)) {
854                        parseKeyStyle(parser, skip);
855                    } else {
856                        throw new XmlParseUtils.IllegalStartTag(parser, TAG_KEY);
857                    }
858                } else if (event == XmlPullParser.END_TAG) {
859                    final String tag = parser.getName();
860                    if (DEBUG) endTag("</%s>", tag);
861                    if (TAG_ROW.equals(tag)) {
862                        if (!skip) {
863                            endRow(row);
864                        }
865                        break;
866                    } else if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag)
867                            || TAG_MERGE.equals(tag)) {
868                        break;
869                    } else {
870                        throw new XmlParseUtils.IllegalEndTag(parser, TAG_KEY);
871                    }
872                }
873            }
874        }
875
876        private void parseKey(XmlPullParser parser, Row row, boolean skip)
877                throws XmlPullParserException, IOException {
878            if (skip) {
879                XmlParseUtils.checkEndTag(TAG_KEY, parser);
880                if (DEBUG) startEndTag("<%s /> skipped", TAG_KEY);
881            } else {
882                final Key key = new Key(mResources, mParams, row, parser, mKeyStyles);
883                if (DEBUG) {
884                    startEndTag("<%s%s %s moreKeys=%s />", TAG_KEY,
885                            (key.isEnabled() ? "" : " disabled"), key,
886                            Arrays.toString(key.mMoreKeys));
887                }
888                XmlParseUtils.checkEndTag(TAG_KEY, parser);
889                endKey(key);
890            }
891        }
892
893        private void parseSpacer(XmlPullParser parser, Row row, boolean skip)
894                throws XmlPullParserException, IOException {
895            if (skip) {
896                XmlParseUtils.checkEndTag(TAG_SPACER, parser);
897                if (DEBUG) startEndTag("<%s /> skipped", TAG_SPACER);
898            } else {
899                final Key.Spacer spacer = new Key.Spacer(
900                        mResources, mParams, row, parser, mKeyStyles);
901                if (DEBUG) startEndTag("<%s />", TAG_SPACER);
902                XmlParseUtils.checkEndTag(TAG_SPACER, parser);
903                endKey(spacer);
904            }
905        }
906
907        private void parseIncludeKeyboardContent(XmlPullParser parser, boolean skip)
908                throws XmlPullParserException, IOException {
909            parseIncludeInternal(parser, null, skip);
910        }
911
912        private void parseIncludeRowContent(XmlPullParser parser, Row row, boolean skip)
913                throws XmlPullParserException, IOException {
914            parseIncludeInternal(parser, row, skip);
915        }
916
917        private void parseIncludeInternal(XmlPullParser parser, Row row, boolean skip)
918                throws XmlPullParserException, IOException {
919            if (skip) {
920                XmlParseUtils.checkEndTag(TAG_INCLUDE, parser);
921                if (DEBUG) startEndTag("</%s> skipped", TAG_INCLUDE);
922            } else {
923                final AttributeSet attr = Xml.asAttributeSet(parser);
924                final TypedArray keyboardAttr = mResources.obtainAttributes(attr,
925                        R.styleable.Keyboard_Include);
926                final TypedArray keyAttr = mResources.obtainAttributes(attr,
927                        R.styleable.Keyboard_Key);
928                int keyboardLayout = 0;
929                float savedDefaultKeyWidth = 0;
930                try {
931                    XmlParseUtils.checkAttributeExists(keyboardAttr,
932                            R.styleable.Keyboard_Include_keyboardLayout, "keyboardLayout",
933                            TAG_INCLUDE, parser);
934                    keyboardLayout = keyboardAttr.getResourceId(
935                            R.styleable.Keyboard_Include_keyboardLayout, 0);
936                    if (row != null) {
937                        savedDefaultKeyWidth = row.getDefaultKeyWidth();
938                        if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyXPos)) {
939                            // Override current x coordinate.
940                            row.setXPos(row.getKeyX(keyAttr));
941                        }
942                        if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyWidth)) {
943                            // Override default key width.
944                            row.setDefaultKeyWidth(row.getKeyWidth(keyAttr));
945                        }
946                    }
947                } finally {
948                    keyboardAttr.recycle();
949                    keyAttr.recycle();
950                }
951
952                XmlParseUtils.checkEndTag(TAG_INCLUDE, parser);
953                if (DEBUG) {
954                    startEndTag("<%s keyboardLayout=%s />",TAG_INCLUDE,
955                            mResources.getResourceEntryName(keyboardLayout));
956                }
957                final XmlResourceParser parserForInclude = mResources.getXml(keyboardLayout);
958                try {
959                    parseMerge(parserForInclude, row, skip);
960                } finally {
961                    if (row != null) {
962                        // Restore default key width.
963                        row.setDefaultKeyWidth(savedDefaultKeyWidth);
964                    }
965                    parserForInclude.close();
966                }
967            }
968        }
969
970        private void parseMerge(XmlPullParser parser, Row row, boolean skip)
971                throws XmlPullParserException, IOException {
972            if (DEBUG) startTag("<%s>", TAG_MERGE);
973            int event;
974            while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
975                if (event == XmlPullParser.START_TAG) {
976                    final String tag = parser.getName();
977                    if (TAG_MERGE.equals(tag)) {
978                        if (row == null) {
979                            parseKeyboardContent(parser, skip);
980                        } else {
981                            parseRowContent(parser, row, skip);
982                        }
983                        break;
984                    } else {
985                        throw new XmlParseUtils.ParseException(
986                                "Included keyboard layout must have <merge> root element", parser);
987                    }
988                }
989            }
990        }
991
992        private void parseSwitchKeyboardContent(XmlPullParser parser, boolean skip)
993                throws XmlPullParserException, IOException {
994            parseSwitchInternal(parser, null, skip);
995        }
996
997        private void parseSwitchRowContent(XmlPullParser parser, Row row, boolean skip)
998                throws XmlPullParserException, IOException {
999            parseSwitchInternal(parser, row, skip);
1000        }
1001
1002        private void parseSwitchInternal(XmlPullParser parser, Row row, boolean skip)
1003                throws XmlPullParserException, IOException {
1004            if (DEBUG) startTag("<%s> %s", TAG_SWITCH, mParams.mId);
1005            boolean selected = false;
1006            int event;
1007            while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
1008                if (event == XmlPullParser.START_TAG) {
1009                    final String tag = parser.getName();
1010                    if (TAG_CASE.equals(tag)) {
1011                        selected |= parseCase(parser, row, selected ? true : skip);
1012                    } else if (TAG_DEFAULT.equals(tag)) {
1013                        selected |= parseDefault(parser, row, selected ? true : skip);
1014                    } else {
1015                        throw new XmlParseUtils.IllegalStartTag(parser, TAG_KEY);
1016                    }
1017                } else if (event == XmlPullParser.END_TAG) {
1018                    final String tag = parser.getName();
1019                    if (TAG_SWITCH.equals(tag)) {
1020                        if (DEBUG) endTag("</%s>", TAG_SWITCH);
1021                        break;
1022                    } else {
1023                        throw new XmlParseUtils.IllegalEndTag(parser, TAG_KEY);
1024                    }
1025                }
1026            }
1027        }
1028
1029        private boolean parseCase(XmlPullParser parser, Row row, boolean skip)
1030                throws XmlPullParserException, IOException {
1031            final boolean selected = parseCaseCondition(parser);
1032            if (row == null) {
1033                // Processing Rows.
1034                parseKeyboardContent(parser, selected ? skip : true);
1035            } else {
1036                // Processing Keys.
1037                parseRowContent(parser, row, selected ? skip : true);
1038            }
1039            return selected;
1040        }
1041
1042        private boolean parseCaseCondition(XmlPullParser parser) {
1043            final KeyboardId id = mParams.mId;
1044            if (id == null)
1045                return true;
1046
1047            final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
1048                    R.styleable.Keyboard_Case);
1049            try {
1050                final boolean keyboardSetElementMatched = matchTypedValue(a,
1051                        R.styleable.Keyboard_Case_keyboardSetElement, id.mElementId,
1052                        KeyboardId.elementIdToName(id.mElementId));
1053                final boolean modeMatched = matchTypedValue(a,
1054                        R.styleable.Keyboard_Case_mode, id.mMode, KeyboardId.modeName(id.mMode));
1055                final boolean navigateNextMatched = matchBoolean(a,
1056                        R.styleable.Keyboard_Case_navigateNext, id.navigateNext());
1057                final boolean navigatePreviousMatched = matchBoolean(a,
1058                        R.styleable.Keyboard_Case_navigatePrevious, id.navigatePrevious());
1059                final boolean passwordInputMatched = matchBoolean(a,
1060                        R.styleable.Keyboard_Case_passwordInput, id.passwordInput());
1061                final boolean clobberSettingsKeyMatched = matchBoolean(a,
1062                        R.styleable.Keyboard_Case_clobberSettingsKey, id.mClobberSettingsKey);
1063                final boolean shortcutKeyEnabledMatched = matchBoolean(a,
1064                        R.styleable.Keyboard_Case_shortcutKeyEnabled, id.mShortcutKeyEnabled);
1065                final boolean hasShortcutKeyMatched = matchBoolean(a,
1066                        R.styleable.Keyboard_Case_hasShortcutKey, id.mHasShortcutKey);
1067                final boolean languageSwitchKeyEnabledMatched = matchBoolean(a,
1068                        R.styleable.Keyboard_Case_languageSwitchKeyEnabled,
1069                        id.mLanguageSwitchKeyEnabled);
1070                final boolean isMultiLineMatched = matchBoolean(a,
1071                        R.styleable.Keyboard_Case_isMultiLine, id.isMultiLine());
1072                final boolean imeActionMatched = matchInteger(a,
1073                        R.styleable.Keyboard_Case_imeAction, id.imeAction());
1074                final boolean localeCodeMatched = matchString(a,
1075                        R.styleable.Keyboard_Case_localeCode, id.mLocale.toString());
1076                final boolean languageCodeMatched = matchString(a,
1077                        R.styleable.Keyboard_Case_languageCode, id.mLocale.getLanguage());
1078                final boolean countryCodeMatched = matchString(a,
1079                        R.styleable.Keyboard_Case_countryCode, id.mLocale.getCountry());
1080                final boolean selected = keyboardSetElementMatched && modeMatched
1081                        && navigateNextMatched && navigatePreviousMatched && passwordInputMatched
1082                        && clobberSettingsKeyMatched && shortcutKeyEnabledMatched
1083                        && hasShortcutKeyMatched && languageSwitchKeyEnabledMatched
1084                        && isMultiLineMatched && imeActionMatched && localeCodeMatched
1085                        && languageCodeMatched && countryCodeMatched;
1086
1087                if (DEBUG) {
1088                    startTag("<%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s>%s", TAG_CASE,
1089                            textAttr(a.getString(R.styleable.Keyboard_Case_keyboardSetElement),
1090                                    "keyboardSetElement"),
1091                            textAttr(a.getString(R.styleable.Keyboard_Case_mode), "mode"),
1092                            textAttr(a.getString(R.styleable.Keyboard_Case_imeAction),
1093                                    "imeAction"),
1094                            booleanAttr(a, R.styleable.Keyboard_Case_navigateNext,
1095                                    "navigateNext"),
1096                            booleanAttr(a, R.styleable.Keyboard_Case_navigatePrevious,
1097                                    "navigatePrevious"),
1098                            booleanAttr(a, R.styleable.Keyboard_Case_clobberSettingsKey,
1099                                    "clobberSettingsKey"),
1100                            booleanAttr(a, R.styleable.Keyboard_Case_passwordInput,
1101                                    "passwordInput"),
1102                            booleanAttr(a, R.styleable.Keyboard_Case_shortcutKeyEnabled,
1103                                    "shortcutKeyEnabled"),
1104                            booleanAttr(a, R.styleable.Keyboard_Case_hasShortcutKey,
1105                                    "hasShortcutKey"),
1106                            booleanAttr(a, R.styleable.Keyboard_Case_languageSwitchKeyEnabled,
1107                                    "languageSwitchKeyEnabled"),
1108                            booleanAttr(a, R.styleable.Keyboard_Case_isMultiLine,
1109                                    "isMultiLine"),
1110                            textAttr(a.getString(R.styleable.Keyboard_Case_localeCode),
1111                                    "localeCode"),
1112                            textAttr(a.getString(R.styleable.Keyboard_Case_languageCode),
1113                                    "languageCode"),
1114                            textAttr(a.getString(R.styleable.Keyboard_Case_countryCode),
1115                                    "countryCode"),
1116                            selected ? "" : " skipped");
1117                }
1118
1119                return selected;
1120            } finally {
1121                a.recycle();
1122            }
1123        }
1124
1125        private static boolean matchInteger(TypedArray a, int index, int value) {
1126            // If <case> does not have "index" attribute, that means this <case> is wild-card for
1127            // the attribute.
1128            return !a.hasValue(index) || a.getInt(index, 0) == value;
1129        }
1130
1131        private static boolean matchBoolean(TypedArray a, int index, boolean value) {
1132            // If <case> does not have "index" attribute, that means this <case> is wild-card for
1133            // the attribute.
1134            return !a.hasValue(index) || a.getBoolean(index, false) == value;
1135        }
1136
1137        private static boolean matchString(TypedArray a, int index, String value) {
1138            // If <case> does not have "index" attribute, that means this <case> is wild-card for
1139            // the attribute.
1140            return !a.hasValue(index)
1141                    || stringArrayContains(a.getString(index).split("\\|"), value);
1142        }
1143
1144        private static boolean matchTypedValue(TypedArray a, int index, int intValue,
1145                String strValue) {
1146            // If <case> does not have "index" attribute, that means this <case> is wild-card for
1147            // the attribute.
1148            final TypedValue v = a.peekValue(index);
1149            if (v == null)
1150                return true;
1151
1152            if (isIntegerValue(v)) {
1153                return intValue == a.getInt(index, 0);
1154            } else if (isStringValue(v)) {
1155                return stringArrayContains(a.getString(index).split("\\|"), strValue);
1156            }
1157            return false;
1158        }
1159
1160        private static boolean stringArrayContains(String[] array, String value) {
1161            for (final String elem : array) {
1162                if (elem.equals(value))
1163                    return true;
1164            }
1165            return false;
1166        }
1167
1168        private boolean parseDefault(XmlPullParser parser, Row row, boolean skip)
1169                throws XmlPullParserException, IOException {
1170            if (DEBUG) startTag("<%s>", TAG_DEFAULT);
1171            if (row == null) {
1172                parseKeyboardContent(parser, skip);
1173            } else {
1174                parseRowContent(parser, row, skip);
1175            }
1176            return true;
1177        }
1178
1179        private void parseKeyStyle(XmlPullParser parser, boolean skip)
1180                throws XmlPullParserException, IOException {
1181            TypedArray keyStyleAttr = mResources.obtainAttributes(Xml.asAttributeSet(parser),
1182                    R.styleable.Keyboard_KeyStyle);
1183            TypedArray keyAttrs = mResources.obtainAttributes(Xml.asAttributeSet(parser),
1184                    R.styleable.Keyboard_Key);
1185            try {
1186                if (!keyStyleAttr.hasValue(R.styleable.Keyboard_KeyStyle_styleName))
1187                    throw new XmlParseUtils.ParseException("<" + TAG_KEY_STYLE
1188                            + "/> needs styleName attribute", parser);
1189                if (DEBUG) {
1190                    startEndTag("<%s styleName=%s />%s", TAG_KEY_STYLE,
1191                        keyStyleAttr.getString(R.styleable.Keyboard_KeyStyle_styleName),
1192                        skip ? " skipped" : "");
1193                }
1194                if (!skip)
1195                    mKeyStyles.parseKeyStyleAttributes(keyStyleAttr, keyAttrs, parser);
1196            } finally {
1197                keyStyleAttr.recycle();
1198                keyAttrs.recycle();
1199            }
1200            XmlParseUtils.checkEndTag(TAG_KEY_STYLE, parser);
1201        }
1202
1203        private void startKeyboard() {
1204            mCurrentY += mParams.mTopPadding;
1205            mTopEdge = true;
1206        }
1207
1208        private void startRow(Row row) {
1209            addEdgeSpace(mParams.mHorizontalEdgesPadding, row);
1210            mCurrentRow = row;
1211            mLeftEdge = true;
1212            mRightEdgeKey = null;
1213        }
1214
1215        private void endRow(Row row) {
1216            if (mCurrentRow == null)
1217                throw new InflateException("orphant end row tag");
1218            if (mRightEdgeKey != null) {
1219                mRightEdgeKey.markAsRightEdge(mParams);
1220                mRightEdgeKey = null;
1221            }
1222            addEdgeSpace(mParams.mHorizontalEdgesPadding, row);
1223            mCurrentY += row.mRowHeight;
1224            mCurrentRow = null;
1225            mTopEdge = false;
1226        }
1227
1228        private void endKey(Key key) {
1229            mParams.onAddKey(key);
1230            if (mLeftEdge) {
1231                key.markAsLeftEdge(mParams);
1232                mLeftEdge = false;
1233            }
1234            if (mTopEdge) {
1235                key.markAsTopEdge(mParams);
1236            }
1237            mRightEdgeKey = key;
1238        }
1239
1240        private void endKeyboard() {
1241            // nothing to do here.
1242        }
1243
1244        private void addEdgeSpace(float width, Row row) {
1245            row.advanceXPos(width);
1246            mLeftEdge = false;
1247            mRightEdgeKey = null;
1248        }
1249
1250        public static float getDimensionOrFraction(TypedArray a, int index, int base,
1251                float defValue) {
1252            final TypedValue value = a.peekValue(index);
1253            if (value == null)
1254                return defValue;
1255            if (isFractionValue(value)) {
1256                return a.getFraction(index, base, base, defValue);
1257            } else if (isDimensionValue(value)) {
1258                return a.getDimension(index, defValue);
1259            }
1260            return defValue;
1261        }
1262
1263        public static int getEnumValue(TypedArray a, int index, int defValue) {
1264            final TypedValue value = a.peekValue(index);
1265            if (value == null)
1266                return defValue;
1267            if (isIntegerValue(value)) {
1268                return a.getInt(index, defValue);
1269            }
1270            return defValue;
1271        }
1272
1273        private static boolean isFractionValue(TypedValue v) {
1274            return v.type == TypedValue.TYPE_FRACTION;
1275        }
1276
1277        private static boolean isDimensionValue(TypedValue v) {
1278            return v.type == TypedValue.TYPE_DIMENSION;
1279        }
1280
1281        private static boolean isIntegerValue(TypedValue v) {
1282            return v.type >= TypedValue.TYPE_FIRST_INT && v.type <= TypedValue.TYPE_LAST_INT;
1283        }
1284
1285        private static boolean isStringValue(TypedValue v) {
1286            return v.type == TypedValue.TYPE_STRING;
1287        }
1288
1289        private static String textAttr(String value, String name) {
1290            return value != null ? String.format(" %s=%s", name, value) : "";
1291        }
1292
1293        private static String booleanAttr(TypedArray a, int index, String name) {
1294            return a.hasValue(index)
1295                    ? String.format(" %s=%s", name, a.getBoolean(index, false)) : "";
1296        }
1297    }
1298}
1299