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