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