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