1/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of 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,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under 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.os.Build;
24import android.text.TextUtils;
25import android.util.AttributeSet;
26import android.util.Log;
27import android.util.TypedValue;
28import android.util.Xml;
29
30import com.android.inputmethod.annotations.UsedForTesting;
31import com.android.inputmethod.keyboard.Key;
32import com.android.inputmethod.keyboard.Keyboard;
33import com.android.inputmethod.keyboard.KeyboardId;
34import com.android.inputmethod.keyboard.KeyboardTheme;
35import com.android.inputmethod.latin.R;
36import com.android.inputmethod.latin.common.Constants;
37import com.android.inputmethod.latin.common.StringUtils;
38import com.android.inputmethod.latin.utils.ResourceUtils;
39import com.android.inputmethod.latin.utils.XmlParseUtils;
40import com.android.inputmethod.latin.utils.XmlParseUtils.ParseException;
41
42import org.xmlpull.v1.XmlPullParser;
43import org.xmlpull.v1.XmlPullParserException;
44
45import java.io.IOException;
46import java.util.Arrays;
47import java.util.Locale;
48
49import javax.annotation.Nonnull;
50
51/**
52 * Keyboard Building helper.
53 *
54 * This class parses Keyboard XML file and eventually build a Keyboard.
55 * The Keyboard XML file looks like:
56 * <pre>
57 *   &lt;!-- xml/keyboard.xml --&gt;
58 *   &lt;Keyboard keyboard_attributes*&gt;
59 *     &lt;!-- Keyboard Content --&gt;
60 *     &lt;Row row_attributes*&gt;
61 *       &lt;!-- Row Content --&gt;
62 *       &lt;Key key_attributes* /&gt;
63 *       &lt;Spacer horizontalGap="32.0dp" /&gt;
64 *       &lt;include keyboardLayout="@xml/other_keys"&gt;
65 *       ...
66 *     &lt;/Row&gt;
67 *     &lt;include keyboardLayout="@xml/other_rows"&gt;
68 *     ...
69 *   &lt;/Keyboard&gt;
70 * </pre>
71 * The XML file which is included in other file must have &lt;merge&gt; as root element,
72 * such as:
73 * <pre>
74 *   &lt;!-- xml/other_keys.xml --&gt;
75 *   &lt;merge&gt;
76 *     &lt;Key key_attributes* /&gt;
77 *     ...
78 *   &lt;/merge&gt;
79 * </pre>
80 * and
81 * <pre>
82 *   &lt;!-- xml/other_rows.xml --&gt;
83 *   &lt;merge&gt;
84 *     &lt;Row row_attributes*&gt;
85 *       &lt;Key key_attributes* /&gt;
86 *     &lt;/Row&gt;
87 *     ...
88 *   &lt;/merge&gt;
89 * </pre>
90 * You can also use switch-case-default tags to select Rows and Keys.
91 * <pre>
92 *   &lt;switch&gt;
93 *     &lt;case case_attribute*&gt;
94 *       &lt;!-- Any valid tags at switch position --&gt;
95 *     &lt;/case&gt;
96 *     ...
97 *     &lt;default&gt;
98 *       &lt;!-- Any valid tags at switch position --&gt;
99 *     &lt;/default&gt;
100 *   &lt;/switch&gt;
101 * </pre>
102 * You can declare Key style and specify styles within Key tags.
103 * <pre>
104 *     &lt;switch&gt;
105 *       &lt;case mode="email"&gt;
106 *         &lt;key-style styleName="f1-key" parentStyle="modifier-key"
107 *           keyLabel=".com"
108 *         /&gt;
109 *       &lt;/case&gt;
110 *       &lt;case mode="url"&gt;
111 *         &lt;key-style styleName="f1-key" parentStyle="modifier-key"
112 *           keyLabel="http://"
113 *         /&gt;
114 *       &lt;/case&gt;
115 *     &lt;/switch&gt;
116 *     ...
117 *     &lt;Key keyStyle="shift-key" ... /&gt;
118 * </pre>
119 */
120
121// TODO: Write unit tests for this class.
122public class KeyboardBuilder<KP extends KeyboardParams> {
123    private static final String BUILDER_TAG = "Keyboard.Builder";
124    private static final boolean DEBUG = false;
125
126    // Keyboard XML Tags
127    private static final String TAG_KEYBOARD = "Keyboard";
128    private static final String TAG_ROW = "Row";
129    private static final String TAG_GRID_ROWS = "GridRows";
130    private static final String TAG_KEY = "Key";
131    private static final String TAG_SPACER = "Spacer";
132    private static final String TAG_INCLUDE = "include";
133    private static final String TAG_MERGE = "merge";
134    private static final String TAG_SWITCH = "switch";
135    private static final String TAG_CASE = "case";
136    private static final String TAG_DEFAULT = "default";
137    public static final String TAG_KEY_STYLE = "key-style";
138
139    private static final int DEFAULT_KEYBOARD_COLUMNS = 10;
140    private static final int DEFAULT_KEYBOARD_ROWS = 4;
141
142    @Nonnull
143    protected final KP mParams;
144    protected final Context mContext;
145    protected final Resources mResources;
146
147    private int mCurrentY = 0;
148    private KeyboardRow mCurrentRow = null;
149    private boolean mLeftEdge;
150    private boolean mTopEdge;
151    private Key mRightEdgeKey = null;
152
153    public KeyboardBuilder(final Context context, @Nonnull final KP params) {
154        mContext = context;
155        final Resources res = context.getResources();
156        mResources = res;
157
158        mParams = params;
159
160        params.GRID_WIDTH = res.getInteger(R.integer.config_keyboard_grid_width);
161        params.GRID_HEIGHT = res.getInteger(R.integer.config_keyboard_grid_height);
162    }
163
164    public void setAllowRedundantMoreKes(final boolean enabled) {
165        mParams.mAllowRedundantMoreKeys = enabled;
166    }
167
168    public KeyboardBuilder<KP> load(final int xmlId, final KeyboardId id) {
169        mParams.mId = id;
170        final XmlResourceParser parser = mResources.getXml(xmlId);
171        try {
172            parseKeyboard(parser);
173        } catch (XmlPullParserException e) {
174            Log.w(BUILDER_TAG, "keyboard XML parse error", e);
175            throw new IllegalArgumentException(e.getMessage(), e);
176        } catch (IOException e) {
177            Log.w(BUILDER_TAG, "keyboard XML parse error", e);
178            throw new RuntimeException(e.getMessage(), e);
179        } finally {
180            parser.close();
181        }
182        return this;
183    }
184
185    @UsedForTesting
186    public void disableTouchPositionCorrectionDataForTest() {
187        mParams.mTouchPositionCorrection.setEnabled(false);
188    }
189
190    public void setProximityCharsCorrectionEnabled(final boolean enabled) {
191        mParams.mProximityCharsCorrectionEnabled = enabled;
192    }
193
194    @Nonnull
195    public Keyboard build() {
196        return new Keyboard(mParams);
197    }
198
199    private int mIndent;
200    private static final String SPACES = "                                             ";
201
202    private static String spaces(final int count) {
203        return (count < SPACES.length()) ? SPACES.substring(0, count) : SPACES;
204    }
205
206    private void startTag(final String format, final Object ... args) {
207        Log.d(BUILDER_TAG, String.format(spaces(++mIndent * 2) + format, args));
208    }
209
210    private void endTag(final String format, final Object ... args) {
211        Log.d(BUILDER_TAG, String.format(spaces(mIndent-- * 2) + format, args));
212    }
213
214    private void startEndTag(final String format, final Object ... args) {
215        Log.d(BUILDER_TAG, String.format(spaces(++mIndent * 2) + format, args));
216        mIndent--;
217    }
218
219    private void parseKeyboard(final XmlPullParser parser)
220            throws XmlPullParserException, IOException {
221        if (DEBUG) startTag("<%s> %s", TAG_KEYBOARD, mParams.mId);
222        while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
223            final int event = parser.next();
224            if (event == XmlPullParser.START_TAG) {
225                final String tag = parser.getName();
226                if (TAG_KEYBOARD.equals(tag)) {
227                    parseKeyboardAttributes(parser);
228                    startKeyboard();
229                    parseKeyboardContent(parser, false);
230                    return;
231                }
232                throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD);
233            }
234        }
235    }
236
237    private void parseKeyboardAttributes(final XmlPullParser parser) {
238        final AttributeSet attr = Xml.asAttributeSet(parser);
239        final TypedArray keyboardAttr = mContext.obtainStyledAttributes(
240                attr, R.styleable.Keyboard, R.attr.keyboardStyle, R.style.Keyboard);
241        final TypedArray keyAttr = mResources.obtainAttributes(attr, R.styleable.Keyboard_Key);
242        try {
243            final KeyboardParams params = mParams;
244            final int height = params.mId.mHeight;
245            final int width = params.mId.mWidth;
246            params.mOccupiedHeight = height;
247            params.mOccupiedWidth = width;
248            params.mTopPadding = (int)keyboardAttr.getFraction(
249                    R.styleable.Keyboard_keyboardTopPadding, height, height, 0);
250            params.mBottomPadding = (int)keyboardAttr.getFraction(
251                    R.styleable.Keyboard_keyboardBottomPadding, height, height, 0);
252            params.mLeftPadding = (int)keyboardAttr.getFraction(
253                    R.styleable.Keyboard_keyboardLeftPadding, width, width, 0);
254            params.mRightPadding = (int)keyboardAttr.getFraction(
255                    R.styleable.Keyboard_keyboardRightPadding, width, width, 0);
256
257            final int baseWidth =
258                    params.mOccupiedWidth - params.mLeftPadding - params.mRightPadding;
259            params.mBaseWidth = baseWidth;
260            params.mDefaultKeyWidth = (int)keyAttr.getFraction(R.styleable.Keyboard_Key_keyWidth,
261                    baseWidth, baseWidth, baseWidth / DEFAULT_KEYBOARD_COLUMNS);
262            params.mHorizontalGap = (int)keyboardAttr.getFraction(
263                    R.styleable.Keyboard_horizontalGap, baseWidth, baseWidth, 0);
264            // TODO: Fix keyboard geometry calculation clearer. Historically vertical gap between
265            // rows are determined based on the entire keyboard height including top and bottom
266            // paddings.
267            params.mVerticalGap = (int)keyboardAttr.getFraction(
268                    R.styleable.Keyboard_verticalGap, height, height, 0);
269            final int baseHeight = params.mOccupiedHeight - params.mTopPadding
270                    - params.mBottomPadding + params.mVerticalGap;
271            params.mBaseHeight = baseHeight;
272            params.mDefaultRowHeight = (int)ResourceUtils.getDimensionOrFraction(keyboardAttr,
273                    R.styleable.Keyboard_rowHeight, baseHeight, baseHeight / DEFAULT_KEYBOARD_ROWS);
274
275            params.mKeyVisualAttributes = KeyVisualAttributes.newInstance(keyAttr);
276
277            params.mMoreKeysTemplate = keyboardAttr.getResourceId(
278                    R.styleable.Keyboard_moreKeysTemplate, 0);
279            params.mMaxMoreKeysKeyboardColumn = keyAttr.getInt(
280                    R.styleable.Keyboard_Key_maxMoreKeysColumn, 5);
281
282            params.mThemeId = keyboardAttr.getInt(R.styleable.Keyboard_themeId, 0);
283            params.mIconsSet.loadIcons(keyboardAttr);
284            params.mTextsSet.setLocale(params.mId.getLocale(), mContext);
285
286            final int resourceId = keyboardAttr.getResourceId(
287                    R.styleable.Keyboard_touchPositionCorrectionData, 0);
288            if (resourceId != 0) {
289                final String[] data = mResources.getStringArray(resourceId);
290                params.mTouchPositionCorrection.load(data);
291            }
292        } finally {
293            keyAttr.recycle();
294            keyboardAttr.recycle();
295        }
296    }
297
298    private void parseKeyboardContent(final XmlPullParser parser, final boolean skip)
299            throws XmlPullParserException, IOException {
300        while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
301            final int event = parser.next();
302            if (event == XmlPullParser.START_TAG) {
303                final String tag = parser.getName();
304                if (TAG_ROW.equals(tag)) {
305                    final KeyboardRow row = parseRowAttributes(parser);
306                    if (DEBUG) startTag("<%s>%s", TAG_ROW, skip ? " skipped" : "");
307                    if (!skip) {
308                        startRow(row);
309                    }
310                    parseRowContent(parser, row, skip);
311                } else if (TAG_GRID_ROWS.equals(tag)) {
312                    if (DEBUG) startTag("<%s>%s", TAG_GRID_ROWS, skip ? " skipped" : "");
313                    parseGridRows(parser, skip);
314                } else if (TAG_INCLUDE.equals(tag)) {
315                    parseIncludeKeyboardContent(parser, skip);
316                } else if (TAG_SWITCH.equals(tag)) {
317                    parseSwitchKeyboardContent(parser, skip);
318                } else if (TAG_KEY_STYLE.equals(tag)) {
319                    parseKeyStyle(parser, skip);
320                } else {
321                    throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_ROW);
322                }
323            } else if (event == XmlPullParser.END_TAG) {
324                final String tag = parser.getName();
325                if (DEBUG) endTag("</%s>", tag);
326                if (TAG_KEYBOARD.equals(tag)) {
327                    endKeyboard();
328                    return;
329                }
330                if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag) || TAG_MERGE.equals(tag)) {
331                    return;
332                }
333                throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_ROW);
334            }
335        }
336    }
337
338    private KeyboardRow parseRowAttributes(final XmlPullParser parser)
339            throws XmlPullParserException {
340        final AttributeSet attr = Xml.asAttributeSet(parser);
341        final TypedArray keyboardAttr = mResources.obtainAttributes(attr, R.styleable.Keyboard);
342        try {
343            if (keyboardAttr.hasValue(R.styleable.Keyboard_horizontalGap)) {
344                throw new XmlParseUtils.IllegalAttribute(parser, TAG_ROW, "horizontalGap");
345            }
346            if (keyboardAttr.hasValue(R.styleable.Keyboard_verticalGap)) {
347                throw new XmlParseUtils.IllegalAttribute(parser, TAG_ROW, "verticalGap");
348            }
349            return new KeyboardRow(mResources, mParams, parser, mCurrentY);
350        } finally {
351            keyboardAttr.recycle();
352        }
353    }
354
355    private void parseRowContent(final XmlPullParser parser, final KeyboardRow row,
356            final boolean skip) throws XmlPullParserException, IOException {
357        while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
358            final int event = parser.next();
359            if (event == XmlPullParser.START_TAG) {
360                final String tag = parser.getName();
361                if (TAG_KEY.equals(tag)) {
362                    parseKey(parser, row, skip);
363                } else if (TAG_SPACER.equals(tag)) {
364                    parseSpacer(parser, row, skip);
365                } else if (TAG_INCLUDE.equals(tag)) {
366                    parseIncludeRowContent(parser, row, skip);
367                } else if (TAG_SWITCH.equals(tag)) {
368                    parseSwitchRowContent(parser, row, skip);
369                } else if (TAG_KEY_STYLE.equals(tag)) {
370                    parseKeyStyle(parser, skip);
371                } else {
372                    throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_ROW);
373                }
374            } else if (event == XmlPullParser.END_TAG) {
375                final String tag = parser.getName();
376                if (DEBUG) endTag("</%s>", tag);
377                if (TAG_ROW.equals(tag)) {
378                    if (!skip) {
379                        endRow(row);
380                    }
381                    return;
382                }
383                if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag) || TAG_MERGE.equals(tag)) {
384                    return;
385                }
386                throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_ROW);
387            }
388        }
389    }
390
391    private void parseGridRows(final XmlPullParser parser, final boolean skip)
392            throws XmlPullParserException, IOException {
393        if (skip) {
394            XmlParseUtils.checkEndTag(TAG_GRID_ROWS, parser);
395            if (DEBUG) {
396                startEndTag("<%s /> skipped", TAG_GRID_ROWS);
397            }
398            return;
399        }
400        final KeyboardRow gridRows = new KeyboardRow(mResources, mParams, parser, mCurrentY);
401        final TypedArray gridRowAttr = mResources.obtainAttributes(
402                Xml.asAttributeSet(parser), R.styleable.Keyboard_GridRows);
403        final int codesArrayId = gridRowAttr.getResourceId(
404                R.styleable.Keyboard_GridRows_codesArray, 0);
405        final int textsArrayId = gridRowAttr.getResourceId(
406                R.styleable.Keyboard_GridRows_textsArray, 0);
407        gridRowAttr.recycle();
408        if (codesArrayId == 0 && textsArrayId == 0) {
409            throw new XmlParseUtils.ParseException(
410                    "Missing codesArray or textsArray attributes", parser);
411        }
412        if (codesArrayId != 0 && textsArrayId != 0) {
413            throw new XmlParseUtils.ParseException(
414                    "Both codesArray and textsArray attributes specifed", parser);
415        }
416        final String[] array = mResources.getStringArray(
417                codesArrayId != 0 ? codesArrayId : textsArrayId);
418        final int counts = array.length;
419        final float keyWidth = gridRows.getKeyWidth(null, 0.0f);
420        final int numColumns = (int)(mParams.mOccupiedWidth / keyWidth);
421        for (int index = 0; index < counts; index += numColumns) {
422            final KeyboardRow row = new KeyboardRow(mResources, mParams, parser, mCurrentY);
423            startRow(row);
424            for (int c = 0; c < numColumns; c++) {
425                final int i = index + c;
426                if (i >= counts) {
427                    break;
428                }
429                final String label;
430                final int code;
431                final String outputText;
432                final int supportedMinSdkVersion;
433                if (codesArrayId != 0) {
434                    final String codeArraySpec = array[i];
435                    label = CodesArrayParser.parseLabel(codeArraySpec);
436                    code = CodesArrayParser.parseCode(codeArraySpec);
437                    outputText = CodesArrayParser.parseOutputText(codeArraySpec);
438                    supportedMinSdkVersion =
439                            CodesArrayParser.getMinSupportSdkVersion(codeArraySpec);
440                } else {
441                    final String textArraySpec = array[i];
442                    // TODO: Utilize KeySpecParser or write more generic TextsArrayParser.
443                    label = textArraySpec;
444                    code = Constants.CODE_OUTPUT_TEXT;
445                    outputText = textArraySpec + (char)Constants.CODE_SPACE;
446                    supportedMinSdkVersion = 0;
447                }
448                if (Build.VERSION.SDK_INT < supportedMinSdkVersion) {
449                    continue;
450                }
451                final int labelFlags = row.getDefaultKeyLabelFlags();
452                // TODO: Should be able to assign default keyActionFlags as well.
453                final int backgroundType = row.getDefaultBackgroundType();
454                final int x = (int)row.getKeyX(null);
455                final int y = row.getKeyY();
456                final int width = (int)keyWidth;
457                final int height = row.getRowHeight();
458                final Key key = new Key(label, KeyboardIconsSet.ICON_UNDEFINED, code, outputText,
459                        null /* hintLabel */, labelFlags, backgroundType, x, y, width, height,
460                        mParams.mHorizontalGap, mParams.mVerticalGap);
461                endKey(key);
462                row.advanceXPos(keyWidth);
463            }
464            endRow(row);
465        }
466
467        XmlParseUtils.checkEndTag(TAG_GRID_ROWS, parser);
468    }
469
470    private void parseKey(final XmlPullParser parser, final KeyboardRow row, final boolean skip)
471            throws XmlPullParserException, IOException {
472        if (skip) {
473            XmlParseUtils.checkEndTag(TAG_KEY, parser);
474            if (DEBUG) startEndTag("<%s /> skipped", TAG_KEY);
475            return;
476        }
477        final TypedArray keyAttr = mResources.obtainAttributes(
478                Xml.asAttributeSet(parser), R.styleable.Keyboard_Key);
479        final KeyStyle keyStyle = mParams.mKeyStyles.getKeyStyle(keyAttr, parser);
480        final String keySpec = keyStyle.getString(keyAttr, R.styleable.Keyboard_Key_keySpec);
481        if (TextUtils.isEmpty(keySpec)) {
482            throw new ParseException("Empty keySpec", parser);
483        }
484        final Key key = new Key(keySpec, keyAttr, keyStyle, mParams, row);
485        keyAttr.recycle();
486        if (DEBUG) {
487            startEndTag("<%s%s %s moreKeys=%s />", TAG_KEY, (key.isEnabled() ? "" : " disabled"),
488                    key, Arrays.toString(key.getMoreKeys()));
489        }
490        XmlParseUtils.checkEndTag(TAG_KEY, parser);
491        endKey(key);
492    }
493
494    private void parseSpacer(final XmlPullParser parser, final KeyboardRow row, final boolean skip)
495            throws XmlPullParserException, IOException {
496        if (skip) {
497            XmlParseUtils.checkEndTag(TAG_SPACER, parser);
498            if (DEBUG) startEndTag("<%s /> skipped", TAG_SPACER);
499            return;
500        }
501        final TypedArray keyAttr = mResources.obtainAttributes(
502                Xml.asAttributeSet(parser), R.styleable.Keyboard_Key);
503        final KeyStyle keyStyle = mParams.mKeyStyles.getKeyStyle(keyAttr, parser);
504        final Key spacer = new Key.Spacer(keyAttr, keyStyle, mParams, row);
505        keyAttr.recycle();
506        if (DEBUG) startEndTag("<%s />", TAG_SPACER);
507        XmlParseUtils.checkEndTag(TAG_SPACER, parser);
508        endKey(spacer);
509    }
510
511    private void parseIncludeKeyboardContent(final XmlPullParser parser, final boolean skip)
512            throws XmlPullParserException, IOException {
513        parseIncludeInternal(parser, null, skip);
514    }
515
516    private void parseIncludeRowContent(final XmlPullParser parser, final KeyboardRow row,
517            final boolean skip) throws XmlPullParserException, IOException {
518        parseIncludeInternal(parser, row, skip);
519    }
520
521    private void parseIncludeInternal(final XmlPullParser parser, final KeyboardRow row,
522            final boolean skip) throws XmlPullParserException, IOException {
523        if (skip) {
524            XmlParseUtils.checkEndTag(TAG_INCLUDE, parser);
525            if (DEBUG) startEndTag("</%s> skipped", TAG_INCLUDE);
526            return;
527        }
528        final AttributeSet attr = Xml.asAttributeSet(parser);
529        final TypedArray keyboardAttr = mResources.obtainAttributes(
530                attr, R.styleable.Keyboard_Include);
531        final TypedArray keyAttr = mResources.obtainAttributes(attr, R.styleable.Keyboard_Key);
532        int keyboardLayout = 0;
533        try {
534            XmlParseUtils.checkAttributeExists(
535                    keyboardAttr, R.styleable.Keyboard_Include_keyboardLayout, "keyboardLayout",
536                    TAG_INCLUDE, parser);
537            keyboardLayout = keyboardAttr.getResourceId(
538                    R.styleable.Keyboard_Include_keyboardLayout, 0);
539            if (row != null) {
540                // Override current x coordinate.
541                row.setXPos(row.getKeyX(keyAttr));
542                // Push current Row attributes and update with new attributes.
543                row.pushRowAttributes(keyAttr);
544            }
545        } finally {
546            keyboardAttr.recycle();
547            keyAttr.recycle();
548        }
549
550        XmlParseUtils.checkEndTag(TAG_INCLUDE, parser);
551        if (DEBUG) {
552            startEndTag("<%s keyboardLayout=%s />",TAG_INCLUDE,
553                    mResources.getResourceEntryName(keyboardLayout));
554        }
555        final XmlResourceParser parserForInclude = mResources.getXml(keyboardLayout);
556        try {
557            parseMerge(parserForInclude, row, skip);
558        } finally {
559            if (row != null) {
560                // Restore Row attributes.
561                row.popRowAttributes();
562            }
563            parserForInclude.close();
564        }
565    }
566
567    private void parseMerge(final XmlPullParser parser, final KeyboardRow row, final boolean skip)
568            throws XmlPullParserException, IOException {
569        if (DEBUG) startTag("<%s>", TAG_MERGE);
570        while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
571            final int event = parser.next();
572            if (event == XmlPullParser.START_TAG) {
573                final String tag = parser.getName();
574                if (TAG_MERGE.equals(tag)) {
575                    if (row == null) {
576                        parseKeyboardContent(parser, skip);
577                    } else {
578                        parseRowContent(parser, row, skip);
579                    }
580                    return;
581                }
582                throw new XmlParseUtils.ParseException(
583                        "Included keyboard layout must have <merge> root element", parser);
584            }
585        }
586    }
587
588    private void parseSwitchKeyboardContent(final XmlPullParser parser, final boolean skip)
589            throws XmlPullParserException, IOException {
590        parseSwitchInternal(parser, null, skip);
591    }
592
593    private void parseSwitchRowContent(final XmlPullParser parser, final KeyboardRow row,
594            final boolean skip) throws XmlPullParserException, IOException {
595        parseSwitchInternal(parser, row, skip);
596    }
597
598    private void parseSwitchInternal(final XmlPullParser parser, final KeyboardRow row,
599            final boolean skip) throws XmlPullParserException, IOException {
600        if (DEBUG) startTag("<%s> %s", TAG_SWITCH, mParams.mId);
601        boolean selected = false;
602        while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
603            final int event = parser.next();
604            if (event == XmlPullParser.START_TAG) {
605                final String tag = parser.getName();
606                if (TAG_CASE.equals(tag)) {
607                    selected |= parseCase(parser, row, selected ? true : skip);
608                } else if (TAG_DEFAULT.equals(tag)) {
609                    selected |= parseDefault(parser, row, selected ? true : skip);
610                } else {
611                    throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_SWITCH);
612                }
613            } else if (event == XmlPullParser.END_TAG) {
614                final String tag = parser.getName();
615                if (TAG_SWITCH.equals(tag)) {
616                    if (DEBUG) endTag("</%s>", TAG_SWITCH);
617                    return;
618                }
619                throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_SWITCH);
620            }
621        }
622    }
623
624    private boolean parseCase(final XmlPullParser parser, final KeyboardRow row, final boolean skip)
625            throws XmlPullParserException, IOException {
626        final boolean selected = parseCaseCondition(parser);
627        if (row == null) {
628            // Processing Rows.
629            parseKeyboardContent(parser, selected ? skip : true);
630        } else {
631            // Processing Keys.
632            parseRowContent(parser, row, selected ? skip : true);
633        }
634        return selected;
635    }
636
637    private boolean parseCaseCondition(final XmlPullParser parser) {
638        final KeyboardId id = mParams.mId;
639        if (id == null) {
640            return true;
641        }
642        final AttributeSet attr = Xml.asAttributeSet(parser);
643        final TypedArray caseAttr = mResources.obtainAttributes(attr, R.styleable.Keyboard_Case);
644        try {
645            final boolean keyboardLayoutSetMatched = matchString(caseAttr,
646                    R.styleable.Keyboard_Case_keyboardLayoutSet,
647                    id.mSubtype.getKeyboardLayoutSetName());
648            final boolean keyboardLayoutSetElementMatched = matchTypedValue(caseAttr,
649                    R.styleable.Keyboard_Case_keyboardLayoutSetElement, id.mElementId,
650                    KeyboardId.elementIdToName(id.mElementId));
651            final boolean keyboardThemeMacthed = matchTypedValue(caseAttr,
652                    R.styleable.Keyboard_Case_keyboardTheme, mParams.mThemeId,
653                    KeyboardTheme.getKeyboardThemeName(mParams.mThemeId));
654            final boolean modeMatched = matchTypedValue(caseAttr,
655                    R.styleable.Keyboard_Case_mode, id.mMode, KeyboardId.modeName(id.mMode));
656            final boolean navigateNextMatched = matchBoolean(caseAttr,
657                    R.styleable.Keyboard_Case_navigateNext, id.navigateNext());
658            final boolean navigatePreviousMatched = matchBoolean(caseAttr,
659                    R.styleable.Keyboard_Case_navigatePrevious, id.navigatePrevious());
660            final boolean passwordInputMatched = matchBoolean(caseAttr,
661                    R.styleable.Keyboard_Case_passwordInput, id.passwordInput());
662            final boolean clobberSettingsKeyMatched = matchBoolean(caseAttr,
663                    R.styleable.Keyboard_Case_clobberSettingsKey, id.mClobberSettingsKey);
664            final boolean hasShortcutKeyMatched = matchBoolean(caseAttr,
665                    R.styleable.Keyboard_Case_hasShortcutKey, id.mHasShortcutKey);
666            final boolean languageSwitchKeyEnabledMatched = matchBoolean(caseAttr,
667                    R.styleable.Keyboard_Case_languageSwitchKeyEnabled,
668                    id.mLanguageSwitchKeyEnabled);
669            final boolean isMultiLineMatched = matchBoolean(caseAttr,
670                    R.styleable.Keyboard_Case_isMultiLine, id.isMultiLine());
671            final boolean imeActionMatched = matchInteger(caseAttr,
672                    R.styleable.Keyboard_Case_imeAction, id.imeAction());
673            final boolean isIconDefinedMatched = isIconDefined(caseAttr,
674                    R.styleable.Keyboard_Case_isIconDefined, mParams.mIconsSet);
675            final Locale locale = id.getLocale();
676            final boolean localeCodeMatched = matchLocaleCodes(caseAttr, locale);
677            final boolean languageCodeMatched = matchLanguageCodes(caseAttr, locale);
678            final boolean countryCodeMatched = matchCountryCodes(caseAttr, locale);
679            final boolean splitLayoutMatched = matchBoolean(caseAttr,
680                    R.styleable.Keyboard_Case_isSplitLayout, id.mIsSplitLayout);
681            final boolean selected = keyboardLayoutSetMatched && keyboardLayoutSetElementMatched
682                    && keyboardThemeMacthed && modeMatched && navigateNextMatched
683                    && navigatePreviousMatched && passwordInputMatched && clobberSettingsKeyMatched
684                    && hasShortcutKeyMatched  && languageSwitchKeyEnabledMatched
685                    && isMultiLineMatched && imeActionMatched && isIconDefinedMatched
686                    && localeCodeMatched && languageCodeMatched && countryCodeMatched
687                    && splitLayoutMatched;
688
689            if (DEBUG) {
690                startTag("<%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s>%s", TAG_CASE,
691                        textAttr(caseAttr.getString(
692                                R.styleable.Keyboard_Case_keyboardLayoutSet), "keyboardLayoutSet"),
693                        textAttr(caseAttr.getString(
694                                R.styleable.Keyboard_Case_keyboardLayoutSetElement),
695                                "keyboardLayoutSetElement"),
696                        textAttr(caseAttr.getString(
697                                R.styleable.Keyboard_Case_keyboardTheme), "keyboardTheme"),
698                        textAttr(caseAttr.getString(R.styleable.Keyboard_Case_mode), "mode"),
699                        textAttr(caseAttr.getString(R.styleable.Keyboard_Case_imeAction),
700                                "imeAction"),
701                        booleanAttr(caseAttr, R.styleable.Keyboard_Case_navigateNext,
702                                "navigateNext"),
703                        booleanAttr(caseAttr, R.styleable.Keyboard_Case_navigatePrevious,
704                                "navigatePrevious"),
705                        booleanAttr(caseAttr, R.styleable.Keyboard_Case_clobberSettingsKey,
706                                "clobberSettingsKey"),
707                        booleanAttr(caseAttr, R.styleable.Keyboard_Case_passwordInput,
708                                "passwordInput"),
709                        booleanAttr(caseAttr, R.styleable.Keyboard_Case_hasShortcutKey,
710                                "hasShortcutKey"),
711                        booleanAttr(caseAttr, R.styleable.Keyboard_Case_languageSwitchKeyEnabled,
712                                "languageSwitchKeyEnabled"),
713                        booleanAttr(caseAttr, R.styleable.Keyboard_Case_isMultiLine,
714                                "isMultiLine"),
715                        booleanAttr(caseAttr, R.styleable.Keyboard_Case_isSplitLayout,
716                                "splitLayout"),
717                        textAttr(caseAttr.getString(R.styleable.Keyboard_Case_isIconDefined),
718                                "isIconDefined"),
719                        textAttr(caseAttr.getString(R.styleable.Keyboard_Case_localeCode),
720                                "localeCode"),
721                        textAttr(caseAttr.getString(R.styleable.Keyboard_Case_languageCode),
722                                "languageCode"),
723                        textAttr(caseAttr.getString(R.styleable.Keyboard_Case_countryCode),
724                                "countryCode"),
725                        selected ? "" : " skipped");
726            }
727
728            return selected;
729        } finally {
730            caseAttr.recycle();
731        }
732    }
733
734    private static boolean matchLocaleCodes(TypedArray caseAttr, final Locale locale) {
735        return matchString(caseAttr, R.styleable.Keyboard_Case_localeCode, locale.toString());
736    }
737
738    private static boolean matchLanguageCodes(TypedArray caseAttr, Locale locale) {
739        return matchString(caseAttr, R.styleable.Keyboard_Case_languageCode, locale.getLanguage());
740    }
741
742    private static boolean matchCountryCodes(TypedArray caseAttr, Locale locale) {
743        return matchString(caseAttr, R.styleable.Keyboard_Case_countryCode, locale.getCountry());
744    }
745
746    private static boolean matchInteger(final TypedArray a, final int index, final int value) {
747        // If <case> does not have "index" attribute, that means this <case> is wild-card for
748        // the attribute.
749        return !a.hasValue(index) || a.getInt(index, 0) == value;
750    }
751
752    private static boolean matchBoolean(final TypedArray a, final int index, final boolean value) {
753        // If <case> does not have "index" attribute, that means this <case> is wild-card for
754        // the attribute.
755        return !a.hasValue(index) || a.getBoolean(index, false) == value;
756    }
757
758    private static boolean matchString(final TypedArray a, final int index, final String value) {
759        // If <case> does not have "index" attribute, that means this <case> is wild-card for
760        // the attribute.
761        return !a.hasValue(index)
762                || StringUtils.containsInArray(value, a.getString(index).split("\\|"));
763    }
764
765    private static boolean matchTypedValue(final TypedArray a, final int index, final int intValue,
766            final String strValue) {
767        // If <case> does not have "index" attribute, that means this <case> is wild-card for
768        // the attribute.
769        final TypedValue v = a.peekValue(index);
770        if (v == null) {
771            return true;
772        }
773        if (ResourceUtils.isIntegerValue(v)) {
774            return intValue == a.getInt(index, 0);
775        }
776        if (ResourceUtils.isStringValue(v)) {
777            return StringUtils.containsInArray(strValue, a.getString(index).split("\\|"));
778        }
779        return false;
780    }
781
782    private static boolean isIconDefined(final TypedArray a, final int index,
783            final KeyboardIconsSet iconsSet) {
784        if (!a.hasValue(index)) {
785            return true;
786        }
787        final String iconName = a.getString(index);
788        final int iconId = KeyboardIconsSet.getIconId(iconName);
789        return iconsSet.getIconDrawable(iconId) != null;
790    }
791
792    private boolean parseDefault(final XmlPullParser parser, final KeyboardRow row,
793            final boolean skip) throws XmlPullParserException, IOException {
794        if (DEBUG) startTag("<%s>", TAG_DEFAULT);
795        if (row == null) {
796            parseKeyboardContent(parser, skip);
797        } else {
798            parseRowContent(parser, row, skip);
799        }
800        return true;
801    }
802
803    private void parseKeyStyle(final XmlPullParser parser, final boolean skip)
804            throws XmlPullParserException, IOException {
805        final AttributeSet attr = Xml.asAttributeSet(parser);
806        final TypedArray keyStyleAttr = mResources.obtainAttributes(
807                attr, R.styleable.Keyboard_KeyStyle);
808        final TypedArray keyAttrs = mResources.obtainAttributes(attr, R.styleable.Keyboard_Key);
809        try {
810            if (!keyStyleAttr.hasValue(R.styleable.Keyboard_KeyStyle_styleName)) {
811                throw new XmlParseUtils.ParseException("<" + TAG_KEY_STYLE
812                        + "/> needs styleName attribute", parser);
813            }
814            if (DEBUG) {
815                startEndTag("<%s styleName=%s />%s", TAG_KEY_STYLE,
816                        keyStyleAttr.getString(R.styleable.Keyboard_KeyStyle_styleName),
817                        skip ? " skipped" : "");
818            }
819            if (!skip) {
820                mParams.mKeyStyles.parseKeyStyleAttributes(keyStyleAttr, keyAttrs, parser);
821            }
822        } finally {
823            keyStyleAttr.recycle();
824            keyAttrs.recycle();
825        }
826        XmlParseUtils.checkEndTag(TAG_KEY_STYLE, parser);
827    }
828
829    private void startKeyboard() {
830        mCurrentY += mParams.mTopPadding;
831        mTopEdge = true;
832    }
833
834    private void startRow(final KeyboardRow row) {
835        addEdgeSpace(mParams.mLeftPadding, row);
836        mCurrentRow = row;
837        mLeftEdge = true;
838        mRightEdgeKey = null;
839    }
840
841    private void endRow(final KeyboardRow row) {
842        if (mCurrentRow == null) {
843            throw new RuntimeException("orphan end row tag");
844        }
845        if (mRightEdgeKey != null) {
846            mRightEdgeKey.markAsRightEdge(mParams);
847            mRightEdgeKey = null;
848        }
849        addEdgeSpace(mParams.mRightPadding, row);
850        mCurrentY += row.getRowHeight();
851        mCurrentRow = null;
852        mTopEdge = false;
853    }
854
855    private void endKey(@Nonnull final Key key) {
856        mParams.onAddKey(key);
857        if (mLeftEdge) {
858            key.markAsLeftEdge(mParams);
859            mLeftEdge = false;
860        }
861        if (mTopEdge) {
862            key.markAsTopEdge(mParams);
863        }
864        mRightEdgeKey = key;
865    }
866
867    private void endKeyboard() {
868        mParams.removeRedundantMoreKeys();
869        // {@link #parseGridRows(XmlPullParser,boolean)} may populate keyboard rows higher than
870        // previously expected.
871        final int actualHeight = mCurrentY - mParams.mVerticalGap + mParams.mBottomPadding;
872        mParams.mOccupiedHeight = Math.max(mParams.mOccupiedHeight, actualHeight);
873    }
874
875    private void addEdgeSpace(final float width, final KeyboardRow row) {
876        row.advanceXPos(width);
877        mLeftEdge = false;
878        mRightEdgeKey = null;
879    }
880
881    private static String textAttr(final String value, final String name) {
882        return value != null ? String.format(" %s=%s", name, value) : "";
883    }
884
885    private static String booleanAttr(final TypedArray a, final int index, final String name) {
886        return a.hasValue(index)
887                ? String.format(" %s=%s", name, a.getBoolean(index, false)) : "";
888    }
889}
890