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