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