KeyboardBuilder.java revision 35ff94547c16c84c5b6fafdae0b4a683be782b97
1/*
2 * Copyright (C) 2012 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.internal;
18
19import android.content.Context;
20import android.content.res.Resources;
21import android.content.res.TypedArray;
22import android.content.res.XmlResourceParser;
23import android.graphics.Typeface;
24import android.util.AttributeSet;
25import android.util.DisplayMetrics;
26import android.util.Log;
27import android.util.TypedValue;
28import android.util.Xml;
29import android.view.InflateException;
30
31import com.android.inputmethod.keyboard.Key;
32import com.android.inputmethod.keyboard.Keyboard;
33import com.android.inputmethod.keyboard.KeyboardId;
34import com.android.inputmethod.latin.LocaleUtils.RunInLocale;
35import com.android.inputmethod.latin.R;
36import com.android.inputmethod.latin.ResourceUtils;
37import com.android.inputmethod.latin.StringUtils;
38import com.android.inputmethod.latin.SubtypeLocale;
39import com.android.inputmethod.latin.XmlParseUtils;
40
41import org.xmlpull.v1.XmlPullParser;
42import org.xmlpull.v1.XmlPullParserException;
43
44import java.io.IOException;
45import java.util.Arrays;
46import java.util.Locale;
47
48/**
49 * Keyboard Building helper.
50 *
51 * This class parses Keyboard XML file and eventually build a Keyboard.
52 * The Keyboard XML file looks like:
53 * <pre>
54 *   &lt;!-- xml/keyboard.xml --&gt;
55 *   &lt;Keyboard keyboard_attributes*&gt;
56 *     &lt;!-- Keyboard Content --&gt;
57 *     &lt;Row row_attributes*&gt;
58 *       &lt;!-- Row Content --&gt;
59 *       &lt;Key key_attributes* /&gt;
60 *       &lt;Spacer horizontalGap="32.0dp" /&gt;
61 *       &lt;include keyboardLayout="@xml/other_keys"&gt;
62 *       ...
63 *     &lt;/Row&gt;
64 *     &lt;include keyboardLayout="@xml/other_rows"&gt;
65 *     ...
66 *   &lt;/Keyboard&gt;
67 * </pre>
68 * The XML file which is included in other file must have &lt;merge&gt; as root element,
69 * such as:
70 * <pre>
71 *   &lt;!-- xml/other_keys.xml --&gt;
72 *   &lt;merge&gt;
73 *     &lt;Key key_attributes* /&gt;
74 *     ...
75 *   &lt;/merge&gt;
76 * </pre>
77 * and
78 * <pre>
79 *   &lt;!-- xml/other_rows.xml --&gt;
80 *   &lt;merge&gt;
81 *     &lt;Row row_attributes*&gt;
82 *       &lt;Key key_attributes* /&gt;
83 *     &lt;/Row&gt;
84 *     ...
85 *   &lt;/merge&gt;
86 * </pre>
87 * You can also use switch-case-default tags to select Rows and Keys.
88 * <pre>
89 *   &lt;switch&gt;
90 *     &lt;case case_attribute*&gt;
91 *       &lt;!-- Any valid tags at switch position --&gt;
92 *     &lt;/case&gt;
93 *     ...
94 *     &lt;default&gt;
95 *       &lt;!-- Any valid tags at switch position --&gt;
96 *     &lt;/default&gt;
97 *   &lt;/switch&gt;
98 * </pre>
99 * You can declare Key style and specify styles within Key tags.
100 * <pre>
101 *     &lt;switch&gt;
102 *       &lt;case mode="email"&gt;
103 *         &lt;key-style styleName="f1-key" parentStyle="modifier-key"
104 *           keyLabel=".com"
105 *         /&gt;
106 *       &lt;/case&gt;
107 *       &lt;case mode="url"&gt;
108 *         &lt;key-style styleName="f1-key" parentStyle="modifier-key"
109 *           keyLabel="http://"
110 *         /&gt;
111 *       &lt;/case&gt;
112 *     &lt;/switch&gt;
113 *     ...
114 *     &lt;Key keyStyle="shift-key" ... /&gt;
115 * </pre>
116 */
117
118public class KeyboardBuilder<KP extends KeyboardParams> {
119    private static final String BUILDER_TAG = "Keyboard.Builder";
120    private static final boolean DEBUG = false;
121
122    // Keyboard XML Tags
123    private static final String TAG_KEYBOARD = "Keyboard";
124    private static final String TAG_ROW = "Row";
125    private static final String TAG_KEY = "Key";
126    private static final String TAG_SPACER = "Spacer";
127    private static final String TAG_INCLUDE = "include";
128    private static final String TAG_MERGE = "merge";
129    private static final String TAG_SWITCH = "switch";
130    private static final String TAG_CASE = "case";
131    private static final String TAG_DEFAULT = "default";
132    public static final String TAG_KEY_STYLE = "key-style";
133
134    private static final int DEFAULT_KEYBOARD_COLUMNS = 10;
135    private static final int DEFAULT_KEYBOARD_ROWS = 4;
136
137    protected final KP mParams;
138    protected final Context mContext;
139    protected final Resources mResources;
140    private final DisplayMetrics mDisplayMetrics;
141
142    private int mCurrentY = 0;
143    private KeyboardRow mCurrentRow = null;
144    private boolean mLeftEdge;
145    private boolean mTopEdge;
146    private Key mRightEdgeKey = null;
147
148    public KeyboardBuilder(final Context context, final KP params) {
149        mContext = context;
150        final Resources res = context.getResources();
151        mResources = res;
152        mDisplayMetrics = res.getDisplayMetrics();
153
154        mParams = params;
155
156        params.GRID_WIDTH = res.getInteger(R.integer.config_keyboard_grid_width);
157        params.GRID_HEIGHT = res.getInteger(R.integer.config_keyboard_grid_height);
158    }
159
160    public void setAutoGenerate(final KeysCache keysCache) {
161        mParams.mKeysCache = keysCache;
162    }
163
164    public KeyboardBuilder<KP> load(final int xmlId, final KeyboardId id) {
165        mParams.mId = id;
166        final XmlResourceParser parser = mResources.getXml(xmlId);
167        try {
168            parseKeyboard(parser);
169        } catch (XmlPullParserException e) {
170            Log.w(BUILDER_TAG, "keyboard XML parse error: " + e);
171            throw new IllegalArgumentException(e);
172        } catch (IOException e) {
173            Log.w(BUILDER_TAG, "keyboard XML parse error: " + e);
174            throw new RuntimeException(e);
175        } finally {
176            parser.close();
177        }
178        return this;
179    }
180
181    // TODO: Remove this method.
182    public void setTouchPositionCorrectionEnabled(final boolean enabled) {
183        mParams.mTouchPositionCorrection.setEnabled(enabled);
184    }
185
186    public void setProximityCharsCorrectionEnabled(final boolean enabled) {
187        mParams.mProximityCharsCorrectionEnabled = enabled;
188    }
189
190    public Keyboard build() {
191        return new Keyboard(mParams);
192    }
193
194    private int mIndent;
195    private static final String SPACES = "                                             ";
196
197    private static String spaces(final int count) {
198        return (count < SPACES.length()) ? SPACES.substring(0, count) : SPACES;
199    }
200
201    private void startTag(final String format, final Object ... args) {
202        Log.d(BUILDER_TAG, String.format(spaces(++mIndent * 2) + format, args));
203    }
204
205    private void endTag(final String format, final Object ... args) {
206        Log.d(BUILDER_TAG, String.format(spaces(mIndent-- * 2) + format, args));
207    }
208
209    private void startEndTag(final String format, final Object ... args) {
210        Log.d(BUILDER_TAG, String.format(spaces(++mIndent * 2) + format, args));
211        mIndent--;
212    }
213
214    private void parseKeyboard(final XmlPullParser parser)
215            throws XmlPullParserException, IOException {
216        if (DEBUG) startTag("<%s> %s", TAG_KEYBOARD, mParams.mId);
217        int event;
218        while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
219            if (event == XmlPullParser.START_TAG) {
220                final String tag = parser.getName();
221                if (TAG_KEYBOARD.equals(tag)) {
222                    parseKeyboardAttributes(parser);
223                    startKeyboard();
224                    parseKeyboardContent(parser, false);
225                    break;
226                } else {
227                    throw new XmlParseUtils.IllegalStartTag(parser, TAG_KEYBOARD);
228                }
229            }
230        }
231    }
232
233    private void parseKeyboardAttributes(final XmlPullParser parser) {
234        final int displayWidth = mDisplayMetrics.widthPixels;
235        final TypedArray keyboardAttr = mContext.obtainStyledAttributes(
236                Xml.asAttributeSet(parser), R.styleable.Keyboard, R.attr.keyboardStyle,
237                R.style.Keyboard);
238        final TypedArray keyAttr = mResources.obtainAttributes(Xml.asAttributeSet(parser),
239                R.styleable.Keyboard_Key);
240        final TypedArray keyboardViewAttr = mResources.obtainAttributes(
241                Xml.asAttributeSet(parser), R.styleable.KeyboardView);
242        try {
243            final int displayHeight = mDisplayMetrics.heightPixels;
244            final String keyboardHeightString = ResourceUtils.getDeviceOverrideValue(
245                    mResources, R.array.keyboard_heights, null);
246            final float keyboardHeight;
247            if (keyboardHeightString != null) {
248                keyboardHeight = Float.parseFloat(keyboardHeightString)
249                        * mDisplayMetrics.density;
250            } else {
251                keyboardHeight = keyboardAttr.getDimension(
252                        R.styleable.Keyboard_keyboardHeight, displayHeight / 2);
253            }
254            final float maxKeyboardHeight = ResourceUtils.getDimensionOrFraction(keyboardAttr,
255                    R.styleable.Keyboard_maxKeyboardHeight, displayHeight, displayHeight / 2);
256            float minKeyboardHeight = ResourceUtils.getDimensionOrFraction(keyboardAttr,
257                    R.styleable.Keyboard_minKeyboardHeight, displayHeight, displayHeight / 2);
258            if (minKeyboardHeight < 0) {
259                // Specified fraction was negative, so it should be calculated against display
260                // width.
261                minKeyboardHeight = -ResourceUtils.getDimensionOrFraction(keyboardAttr,
262                        R.styleable.Keyboard_minKeyboardHeight, displayWidth, displayWidth / 2);
263            }
264            final KeyboardParams params = mParams;
265            // Keyboard height will not exceed maxKeyboardHeight and will not be less than
266            // minKeyboardHeight.
267            params.mOccupiedHeight = (int)Math.max(
268                    Math.min(keyboardHeight, maxKeyboardHeight), minKeyboardHeight);
269            params.mOccupiedWidth = params.mId.mWidth;
270            params.mTopPadding = (int)ResourceUtils.getDimensionOrFraction(keyboardAttr,
271                    R.styleable.Keyboard_keyboardTopPadding, params.mOccupiedHeight, 0);
272            params.mBottomPadding = (int)ResourceUtils.getDimensionOrFraction(keyboardAttr,
273                    R.styleable.Keyboard_keyboardBottomPadding, params.mOccupiedHeight, 0);
274            params.mHorizontalEdgesPadding = (int)ResourceUtils.getDimensionOrFraction(
275                    keyboardAttr,
276                    R.styleable.Keyboard_keyboardHorizontalEdgesPadding,
277                    mParams.mOccupiedWidth, 0);
278
279            params.mBaseWidth = params.mOccupiedWidth - params.mHorizontalEdgesPadding * 2
280                    - params.mHorizontalCenterPadding;
281            params.mDefaultKeyWidth = (int)ResourceUtils.getDimensionOrFraction(keyAttr,
282                    R.styleable.Keyboard_Key_keyWidth, params.mBaseWidth,
283                    params.mBaseWidth / DEFAULT_KEYBOARD_COLUMNS);
284            params.mHorizontalGap = (int)ResourceUtils.getDimensionOrFraction(keyboardAttr,
285                    R.styleable.Keyboard_horizontalGap, params.mBaseWidth, 0);
286            params.mVerticalGap = (int)ResourceUtils.getDimensionOrFraction(keyboardAttr,
287                    R.styleable.Keyboard_verticalGap, params.mOccupiedHeight, 0);
288            params.mBaseHeight = params.mOccupiedHeight - params.mTopPadding
289                    - params.mBottomPadding + params.mVerticalGap;
290            params.mDefaultRowHeight = (int)ResourceUtils.getDimensionOrFraction(keyboardAttr,
291                    R.styleable.Keyboard_rowHeight, params.mBaseHeight,
292                    params.mBaseHeight / DEFAULT_KEYBOARD_ROWS);
293
294            if (keyboardViewAttr.hasValue(R.styleable.KeyboardView_keyTypeface)) {
295                params.mKeyTypeface = Typeface.defaultFromStyle(keyboardViewAttr.getInt(
296                        R.styleable.KeyboardView_keyTypeface, Typeface.NORMAL));
297            }
298            params.mKeyLetterRatio = ResourceUtils.getFraction(keyboardViewAttr,
299                    R.styleable.KeyboardView_keyLetterSize);
300            params.mKeyLetterSize = ResourceUtils.getDimensionPixelSize(keyboardViewAttr,
301                    R.styleable.KeyboardView_keyLetterSize);
302            params.mKeyHintLetterRatio = ResourceUtils.getFraction(keyboardViewAttr,
303                    R.styleable.KeyboardView_keyHintLetterRatio);
304            params.mKeyShiftedLetterHintRatio = ResourceUtils.getFraction(keyboardViewAttr,
305                    R.styleable.KeyboardView_keyShiftedLetterHintRatio);
306
307            params.mMoreKeysTemplate = keyboardAttr.getResourceId(
308                    R.styleable.Keyboard_moreKeysTemplate, 0);
309            params.mMaxMoreKeysKeyboardColumn = keyAttr.getInt(
310                    R.styleable.Keyboard_Key_maxMoreKeysColumn, 5);
311
312            params.mThemeId = keyboardAttr.getInt(R.styleable.Keyboard_themeId, 0);
313            params.mIconsSet.loadIcons(keyboardAttr);
314            final String language = params.mId.mLocale.getLanguage();
315            params.mCodesSet.setLanguage(language);
316            params.mTextsSet.setLanguage(language);
317            final RunInLocale<Void> job = new RunInLocale<Void>() {
318                @Override
319                protected Void job(Resources res) {
320                    params.mTextsSet.loadStringResources(mContext);
321                    return null;
322                }
323            };
324            // Null means the current system locale.
325            final Locale locale = SubtypeLocale.isNoLanguage(params.mId.mSubtype)
326                    ? null : params.mId.mLocale;
327            job.runInLocale(mResources, locale);
328
329            final int resourceId = keyboardAttr.getResourceId(
330                    R.styleable.Keyboard_touchPositionCorrectionData, 0);
331            params.mTouchPositionCorrection.setEnabled(resourceId != 0);
332            if (resourceId != 0) {
333                final String[] data = mResources.getStringArray(resourceId);
334                params.mTouchPositionCorrection.load(data);
335            }
336        } finally {
337            keyboardViewAttr.recycle();
338            keyAttr.recycle();
339            keyboardAttr.recycle();
340        }
341    }
342
343    private void parseKeyboardContent(final XmlPullParser parser, final boolean skip)
344            throws XmlPullParserException, IOException {
345        int event;
346        while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
347            if (event == XmlPullParser.START_TAG) {
348                final String tag = parser.getName();
349                if (TAG_ROW.equals(tag)) {
350                    final KeyboardRow row = parseRowAttributes(parser);
351                    if (DEBUG) startTag("<%s>%s", TAG_ROW, skip ? " skipped" : "");
352                    if (!skip) {
353                        startRow(row);
354                    }
355                    parseRowContent(parser, row, skip);
356                } else if (TAG_INCLUDE.equals(tag)) {
357                    parseIncludeKeyboardContent(parser, skip);
358                } else if (TAG_SWITCH.equals(tag)) {
359                    parseSwitchKeyboardContent(parser, skip);
360                } else if (TAG_KEY_STYLE.equals(tag)) {
361                    parseKeyStyle(parser, skip);
362                } else {
363                    throw new XmlParseUtils.IllegalStartTag(parser, TAG_ROW);
364                }
365            } else if (event == XmlPullParser.END_TAG) {
366                final String tag = parser.getName();
367                if (DEBUG) endTag("</%s>", tag);
368                if (TAG_KEYBOARD.equals(tag)) {
369                    endKeyboard();
370                    break;
371                } else if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag)
372                        || TAG_MERGE.equals(tag)) {
373                    break;
374                } else {
375                    throw new XmlParseUtils.IllegalEndTag(parser, TAG_ROW);
376                }
377            }
378        }
379    }
380
381    private KeyboardRow parseRowAttributes(final XmlPullParser parser)
382            throws XmlPullParserException {
383        final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
384                R.styleable.Keyboard);
385        try {
386            if (a.hasValue(R.styleable.Keyboard_horizontalGap)) {
387                throw new XmlParseUtils.IllegalAttribute(parser, "horizontalGap");
388            }
389            if (a.hasValue(R.styleable.Keyboard_verticalGap)) {
390                throw new XmlParseUtils.IllegalAttribute(parser, "verticalGap");
391            }
392            return new KeyboardRow(mResources, mParams, parser, mCurrentY);
393        } finally {
394            a.recycle();
395        }
396    }
397
398    private void parseRowContent(final XmlPullParser parser, final KeyboardRow row,
399            final boolean skip) throws XmlPullParserException, IOException {
400        int event;
401        while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
402            if (event == XmlPullParser.START_TAG) {
403                final String tag = parser.getName();
404                if (TAG_KEY.equals(tag)) {
405                    parseKey(parser, row, skip);
406                } else if (TAG_SPACER.equals(tag)) {
407                    parseSpacer(parser, row, skip);
408                } else if (TAG_INCLUDE.equals(tag)) {
409                    parseIncludeRowContent(parser, row, skip);
410                } else if (TAG_SWITCH.equals(tag)) {
411                    parseSwitchRowContent(parser, row, skip);
412                } else if (TAG_KEY_STYLE.equals(tag)) {
413                    parseKeyStyle(parser, skip);
414                } else {
415                    throw new XmlParseUtils.IllegalStartTag(parser, TAG_KEY);
416                }
417            } else if (event == XmlPullParser.END_TAG) {
418                final String tag = parser.getName();
419                if (DEBUG) endTag("</%s>", tag);
420                if (TAG_ROW.equals(tag)) {
421                    if (!skip) {
422                        endRow(row);
423                    }
424                    break;
425                } else if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag)
426                        || TAG_MERGE.equals(tag)) {
427                    break;
428                } else {
429                    throw new XmlParseUtils.IllegalEndTag(parser, TAG_KEY);
430                }
431            }
432        }
433    }
434
435    private void parseKey(final XmlPullParser parser, final KeyboardRow row, final boolean skip)
436            throws XmlPullParserException, IOException {
437        if (skip) {
438            XmlParseUtils.checkEndTag(TAG_KEY, parser);
439            if (DEBUG) {
440                startEndTag("<%s /> skipped", TAG_KEY);
441            }
442        } else {
443            final Key key = new Key(mResources, mParams, row, parser);
444            if (DEBUG) {
445                startEndTag("<%s%s %s moreKeys=%s />", TAG_KEY,
446                        (key.isEnabled() ? "" : " disabled"), key,
447                        Arrays.toString(key.mMoreKeys));
448            }
449            XmlParseUtils.checkEndTag(TAG_KEY, parser);
450            endKey(key);
451        }
452    }
453
454    private void parseSpacer(final XmlPullParser parser, final KeyboardRow row, final boolean skip)
455            throws XmlPullParserException, IOException {
456        if (skip) {
457            XmlParseUtils.checkEndTag(TAG_SPACER, parser);
458            if (DEBUG) startEndTag("<%s /> skipped", TAG_SPACER);
459        } else {
460            final Key.Spacer spacer = new Key.Spacer(mResources, mParams, row, parser);
461            if (DEBUG) startEndTag("<%s />", TAG_SPACER);
462            XmlParseUtils.checkEndTag(TAG_SPACER, parser);
463            endKey(spacer);
464        }
465    }
466
467    private void parseIncludeKeyboardContent(final XmlPullParser parser, final boolean skip)
468            throws XmlPullParserException, IOException {
469        parseIncludeInternal(parser, null, skip);
470    }
471
472    private void parseIncludeRowContent(final XmlPullParser parser, final KeyboardRow row,
473            final boolean skip) throws XmlPullParserException, IOException {
474        parseIncludeInternal(parser, row, skip);
475    }
476
477    private void parseIncludeInternal(final XmlPullParser parser, final KeyboardRow row,
478            final boolean skip) throws XmlPullParserException, IOException {
479        if (skip) {
480            XmlParseUtils.checkEndTag(TAG_INCLUDE, parser);
481            if (DEBUG) startEndTag("</%s> skipped", TAG_INCLUDE);
482        } else {
483            final AttributeSet attr = Xml.asAttributeSet(parser);
484            final TypedArray keyboardAttr = mResources.obtainAttributes(attr,
485                    R.styleable.Keyboard_Include);
486            final TypedArray keyAttr = mResources.obtainAttributes(attr,
487                    R.styleable.Keyboard_Key);
488            int keyboardLayout = 0;
489            float savedDefaultKeyWidth = 0;
490            int savedDefaultKeyLabelFlags = 0;
491            int savedDefaultBackgroundType = Key.BACKGROUND_TYPE_NORMAL;
492            try {
493                XmlParseUtils.checkAttributeExists(keyboardAttr,
494                        R.styleable.Keyboard_Include_keyboardLayout, "keyboardLayout",
495                        TAG_INCLUDE, parser);
496                keyboardLayout = keyboardAttr.getResourceId(
497                        R.styleable.Keyboard_Include_keyboardLayout, 0);
498                if (row != null) {
499                    if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyXPos)) {
500                        // Override current x coordinate.
501                        row.setXPos(row.getKeyX(keyAttr));
502                    }
503                    // TODO: Remove this if-clause and do the same as backgroundType below.
504                    savedDefaultKeyWidth = row.getDefaultKeyWidth();
505                    if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyWidth)) {
506                        // Override default key width.
507                        row.setDefaultKeyWidth(row.getKeyWidth(keyAttr));
508                    }
509                    savedDefaultKeyLabelFlags = row.getDefaultKeyLabelFlags();
510                    // Bitwise-or default keyLabelFlag if exists.
511                    row.setDefaultKeyLabelFlags(keyAttr.getInt(
512                            R.styleable.Keyboard_Key_keyLabelFlags, 0)
513                            | savedDefaultKeyLabelFlags);
514                    savedDefaultBackgroundType = row.getDefaultBackgroundType();
515                    // Override default backgroundType if exists.
516                    row.setDefaultBackgroundType(keyAttr.getInt(
517                            R.styleable.Keyboard_Key_backgroundType,
518                            savedDefaultBackgroundType));
519                }
520            } finally {
521                keyboardAttr.recycle();
522                keyAttr.recycle();
523            }
524
525            XmlParseUtils.checkEndTag(TAG_INCLUDE, parser);
526            if (DEBUG) {
527                startEndTag("<%s keyboardLayout=%s />",TAG_INCLUDE,
528                        mResources.getResourceEntryName(keyboardLayout));
529            }
530            final XmlResourceParser parserForInclude = mResources.getXml(keyboardLayout);
531            try {
532                parseMerge(parserForInclude, row, skip);
533            } finally {
534                if (row != null) {
535                    // Restore default keyWidth, keyLabelFlags, and backgroundType.
536                    row.setDefaultKeyWidth(savedDefaultKeyWidth);
537                    row.setDefaultKeyLabelFlags(savedDefaultKeyLabelFlags);
538                    row.setDefaultBackgroundType(savedDefaultBackgroundType);
539                }
540                parserForInclude.close();
541            }
542        }
543    }
544
545    private void parseMerge(final XmlPullParser parser, final KeyboardRow row, final boolean skip)
546            throws XmlPullParserException, IOException {
547        if (DEBUG) startTag("<%s>", TAG_MERGE);
548        int event;
549        while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
550            if (event == XmlPullParser.START_TAG) {
551                final String tag = parser.getName();
552                if (TAG_MERGE.equals(tag)) {
553                    if (row == null) {
554                        parseKeyboardContent(parser, skip);
555                    } else {
556                        parseRowContent(parser, row, skip);
557                    }
558                    break;
559                } else {
560                    throw new XmlParseUtils.ParseException(
561                            "Included keyboard layout must have <merge> root element", parser);
562                }
563            }
564        }
565    }
566
567    private void parseSwitchKeyboardContent(final XmlPullParser parser, final boolean skip)
568            throws XmlPullParserException, IOException {
569        parseSwitchInternal(parser, null, skip);
570    }
571
572    private void parseSwitchRowContent(final XmlPullParser parser, final KeyboardRow row,
573            final boolean skip) throws XmlPullParserException, IOException {
574        parseSwitchInternal(parser, row, skip);
575    }
576
577    private void parseSwitchInternal(final XmlPullParser parser, final KeyboardRow row,
578            final boolean skip) throws XmlPullParserException, IOException {
579        if (DEBUG) startTag("<%s> %s", TAG_SWITCH, mParams.mId);
580        boolean selected = false;
581        int event;
582        while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
583            if (event == XmlPullParser.START_TAG) {
584                final String tag = parser.getName();
585                if (TAG_CASE.equals(tag)) {
586                    selected |= parseCase(parser, row, selected ? true : skip);
587                } else if (TAG_DEFAULT.equals(tag)) {
588                    selected |= parseDefault(parser, row, selected ? true : skip);
589                } else {
590                    throw new XmlParseUtils.IllegalStartTag(parser, TAG_KEY);
591                }
592            } else if (event == XmlPullParser.END_TAG) {
593                final String tag = parser.getName();
594                if (TAG_SWITCH.equals(tag)) {
595                    if (DEBUG) endTag("</%s>", TAG_SWITCH);
596                    break;
597                } else {
598                    throw new XmlParseUtils.IllegalEndTag(parser, TAG_KEY);
599                }
600            }
601        }
602    }
603
604    private boolean parseCase(final XmlPullParser parser, final KeyboardRow row, final boolean skip)
605            throws XmlPullParserException, IOException {
606        final boolean selected = parseCaseCondition(parser);
607        if (row == null) {
608            // Processing Rows.
609            parseKeyboardContent(parser, selected ? skip : true);
610        } else {
611            // Processing Keys.
612            parseRowContent(parser, row, selected ? skip : true);
613        }
614        return selected;
615    }
616
617    private boolean parseCaseCondition(final XmlPullParser parser) {
618        final KeyboardId id = mParams.mId;
619        if (id == null) {
620            return true;
621        }
622        final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
623                R.styleable.Keyboard_Case);
624        try {
625            final boolean keyboardLayoutSetElementMatched = matchTypedValue(a,
626                    R.styleable.Keyboard_Case_keyboardLayoutSetElement, id.mElementId,
627                    KeyboardId.elementIdToName(id.mElementId));
628            final boolean modeMatched = matchTypedValue(a,
629                    R.styleable.Keyboard_Case_mode, id.mMode, KeyboardId.modeName(id.mMode));
630            final boolean navigateNextMatched = matchBoolean(a,
631                    R.styleable.Keyboard_Case_navigateNext, id.navigateNext());
632            final boolean navigatePreviousMatched = matchBoolean(a,
633                    R.styleable.Keyboard_Case_navigatePrevious, id.navigatePrevious());
634            final boolean passwordInputMatched = matchBoolean(a,
635                    R.styleable.Keyboard_Case_passwordInput, id.passwordInput());
636            final boolean clobberSettingsKeyMatched = matchBoolean(a,
637                    R.styleable.Keyboard_Case_clobberSettingsKey, id.mClobberSettingsKey);
638            final boolean shortcutKeyEnabledMatched = matchBoolean(a,
639                    R.styleable.Keyboard_Case_shortcutKeyEnabled, id.mShortcutKeyEnabled);
640            final boolean hasShortcutKeyMatched = matchBoolean(a,
641                    R.styleable.Keyboard_Case_hasShortcutKey, id.mHasShortcutKey);
642            final boolean languageSwitchKeyEnabledMatched = matchBoolean(a,
643                    R.styleable.Keyboard_Case_languageSwitchKeyEnabled,
644                    id.mLanguageSwitchKeyEnabled);
645            final boolean isMultiLineMatched = matchBoolean(a,
646                    R.styleable.Keyboard_Case_isMultiLine, id.isMultiLine());
647            final boolean imeActionMatched = matchInteger(a,
648                    R.styleable.Keyboard_Case_imeAction, id.imeAction());
649            final boolean localeCodeMatched = matchString(a,
650                    R.styleable.Keyboard_Case_localeCode, id.mLocale.toString());
651            final boolean languageCodeMatched = matchString(a,
652                    R.styleable.Keyboard_Case_languageCode, id.mLocale.getLanguage());
653            final boolean countryCodeMatched = matchString(a,
654                    R.styleable.Keyboard_Case_countryCode, id.mLocale.getCountry());
655            final boolean selected = keyboardLayoutSetElementMatched && modeMatched
656                    && navigateNextMatched && navigatePreviousMatched && passwordInputMatched
657                    && clobberSettingsKeyMatched && shortcutKeyEnabledMatched
658                    && hasShortcutKeyMatched && languageSwitchKeyEnabledMatched
659                    && isMultiLineMatched && imeActionMatched && localeCodeMatched
660                    && languageCodeMatched && countryCodeMatched;
661
662            if (DEBUG) {
663                startTag("<%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s>%s", TAG_CASE,
664                        textAttr(a.getString(
665                                R.styleable.Keyboard_Case_keyboardLayoutSetElement),
666                                "keyboardLayoutSetElement"),
667                        textAttr(a.getString(R.styleable.Keyboard_Case_mode), "mode"),
668                        textAttr(a.getString(R.styleable.Keyboard_Case_imeAction),
669                                "imeAction"),
670                        booleanAttr(a, R.styleable.Keyboard_Case_navigateNext,
671                                "navigateNext"),
672                        booleanAttr(a, R.styleable.Keyboard_Case_navigatePrevious,
673                                "navigatePrevious"),
674                        booleanAttr(a, R.styleable.Keyboard_Case_clobberSettingsKey,
675                                "clobberSettingsKey"),
676                        booleanAttr(a, R.styleable.Keyboard_Case_passwordInput,
677                                "passwordInput"),
678                        booleanAttr(a, R.styleable.Keyboard_Case_shortcutKeyEnabled,
679                                "shortcutKeyEnabled"),
680                        booleanAttr(a, R.styleable.Keyboard_Case_hasShortcutKey,
681                                "hasShortcutKey"),
682                        booleanAttr(a, R.styleable.Keyboard_Case_languageSwitchKeyEnabled,
683                                "languageSwitchKeyEnabled"),
684                        booleanAttr(a, R.styleable.Keyboard_Case_isMultiLine,
685                                "isMultiLine"),
686                        textAttr(a.getString(R.styleable.Keyboard_Case_localeCode),
687                                "localeCode"),
688                        textAttr(a.getString(R.styleable.Keyboard_Case_languageCode),
689                                "languageCode"),
690                        textAttr(a.getString(R.styleable.Keyboard_Case_countryCode),
691                                "countryCode"),
692                        selected ? "" : " skipped");
693            }
694
695            return selected;
696        } finally {
697            a.recycle();
698        }
699    }
700
701    private static boolean matchInteger(final TypedArray a, final int index, final int value) {
702        // If <case> does not have "index" attribute, that means this <case> is wild-card for
703        // the attribute.
704        return !a.hasValue(index) || a.getInt(index, 0) == value;
705    }
706
707    private static boolean matchBoolean(final TypedArray a, final int index, final boolean value) {
708        // If <case> does not have "index" attribute, that means this <case> is wild-card for
709        // the attribute.
710        return !a.hasValue(index) || a.getBoolean(index, false) == value;
711    }
712
713    private static boolean matchString(final TypedArray a, final int index, final String value) {
714        // If <case> does not have "index" attribute, that means this <case> is wild-card for
715        // the attribute.
716        return !a.hasValue(index)
717                || StringUtils.containsInArray(value, a.getString(index).split("\\|"));
718    }
719
720    private static boolean matchTypedValue(final TypedArray a, final int index, final int intValue,
721            final String strValue) {
722        // If <case> does not have "index" attribute, that means this <case> is wild-card for
723        // the attribute.
724        final TypedValue v = a.peekValue(index);
725        if (v == null) {
726            return true;
727        }
728        if (ResourceUtils.isIntegerValue(v)) {
729            return intValue == a.getInt(index, 0);
730        } else if (ResourceUtils.isStringValue(v)) {
731            return StringUtils.containsInArray(strValue, a.getString(index).split("\\|"));
732        }
733        return false;
734    }
735
736    private boolean parseDefault(final XmlPullParser parser, final KeyboardRow row,
737            final boolean skip) throws XmlPullParserException, IOException {
738        if (DEBUG) startTag("<%s>", TAG_DEFAULT);
739        if (row == null) {
740            parseKeyboardContent(parser, skip);
741        } else {
742            parseRowContent(parser, row, skip);
743        }
744        return true;
745    }
746
747    private void parseKeyStyle(final XmlPullParser parser, final boolean skip)
748            throws XmlPullParserException, IOException {
749        TypedArray keyStyleAttr = mResources.obtainAttributes(Xml.asAttributeSet(parser),
750                R.styleable.Keyboard_KeyStyle);
751        TypedArray keyAttrs = mResources.obtainAttributes(Xml.asAttributeSet(parser),
752                R.styleable.Keyboard_Key);
753        try {
754            if (!keyStyleAttr.hasValue(R.styleable.Keyboard_KeyStyle_styleName)) {
755                throw new XmlParseUtils.ParseException("<" + TAG_KEY_STYLE
756                        + "/> needs styleName attribute", parser);
757            }
758            if (DEBUG) {
759                startEndTag("<%s styleName=%s />%s", TAG_KEY_STYLE,
760                        keyStyleAttr.getString(R.styleable.Keyboard_KeyStyle_styleName),
761                        skip ? " skipped" : "");
762            }
763            if (!skip) {
764                mParams.mKeyStyles.parseKeyStyleAttributes(keyStyleAttr, keyAttrs, parser);
765            }
766        } finally {
767            keyStyleAttr.recycle();
768            keyAttrs.recycle();
769        }
770        XmlParseUtils.checkEndTag(TAG_KEY_STYLE, parser);
771    }
772
773    private void startKeyboard() {
774        mCurrentY += mParams.mTopPadding;
775        mTopEdge = true;
776    }
777
778    private void startRow(final KeyboardRow row) {
779        addEdgeSpace(mParams.mHorizontalEdgesPadding, row);
780        mCurrentRow = row;
781        mLeftEdge = true;
782        mRightEdgeKey = null;
783    }
784
785    private void endRow(final KeyboardRow row) {
786        if (mCurrentRow == null) {
787            throw new InflateException("orphan end row tag");
788        }
789        if (mRightEdgeKey != null) {
790            mRightEdgeKey.markAsRightEdge(mParams);
791            mRightEdgeKey = null;
792        }
793        addEdgeSpace(mParams.mHorizontalEdgesPadding, row);
794        mCurrentY += row.mRowHeight;
795        mCurrentRow = null;
796        mTopEdge = false;
797    }
798
799    private void endKey(final Key key) {
800        mParams.onAddKey(key);
801        if (mLeftEdge) {
802            key.markAsLeftEdge(mParams);
803            mLeftEdge = false;
804        }
805        if (mTopEdge) {
806            key.markAsTopEdge(mParams);
807        }
808        mRightEdgeKey = key;
809    }
810
811    private void endKeyboard() {
812        // nothing to do here.
813    }
814
815    private void addEdgeSpace(final float width, final KeyboardRow row) {
816        row.advanceXPos(width);
817        mLeftEdge = false;
818        mRightEdgeKey = null;
819    }
820
821    private static String textAttr(final String value, final String name) {
822        return value != null ? String.format(" %s=%s", name, value) : "";
823    }
824
825    private static String booleanAttr(final TypedArray a, final int index, final String name) {
826        return a.hasValue(index)
827                ? String.format(" %s=%s", name, a.getBoolean(index, false)) : "";
828    }
829}
830