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 
17 package com.android.inputmethod.keyboard.internal;
18 
19 import android.content.Context;
20 import android.content.res.Resources;
21 import android.content.res.TypedArray;
22 import android.content.res.XmlResourceParser;
23 import android.os.Build;
24 import android.text.TextUtils;
25 import android.util.AttributeSet;
26 import android.util.Log;
27 import android.util.TypedValue;
28 import android.util.Xml;
29 
30 import com.android.inputmethod.annotations.UsedForTesting;
31 import com.android.inputmethod.keyboard.Key;
32 import com.android.inputmethod.keyboard.Keyboard;
33 import com.android.inputmethod.keyboard.KeyboardId;
34 import com.android.inputmethod.keyboard.KeyboardTheme;
35 import com.android.inputmethod.latin.R;
36 import com.android.inputmethod.latin.common.Constants;
37 import com.android.inputmethod.latin.common.StringUtils;
38 import com.android.inputmethod.latin.utils.ResourceUtils;
39 import com.android.inputmethod.latin.utils.XmlParseUtils;
40 import com.android.inputmethod.latin.utils.XmlParseUtils.ParseException;
41 
42 import org.xmlpull.v1.XmlPullParser;
43 import org.xmlpull.v1.XmlPullParserException;
44 
45 import java.io.IOException;
46 import java.util.Arrays;
47 import java.util.Locale;
48 
49 import 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.
122 public 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 
KeyboardBuilder(final Context context, @Nonnull final KP params)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 
setAllowRedundantMoreKes(final boolean enabled)164     public void setAllowRedundantMoreKes(final boolean enabled) {
165         mParams.mAllowRedundantMoreKeys = enabled;
166     }
167 
load(final int xmlId, final KeyboardId id)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
disableTouchPositionCorrectionDataForTest()186     public void disableTouchPositionCorrectionDataForTest() {
187         mParams.mTouchPositionCorrection.setEnabled(false);
188     }
189 
setProximityCharsCorrectionEnabled(final boolean enabled)190     public void setProximityCharsCorrectionEnabled(final boolean enabled) {
191         mParams.mProximityCharsCorrectionEnabled = enabled;
192     }
193 
194     @Nonnull
build()195     public Keyboard build() {
196         return new Keyboard(mParams);
197     }
198 
199     private int mIndent;
200     private static final String SPACES = "                                             ";
201 
spaces(final int count)202     private static String spaces(final int count) {
203         return (count < SPACES.length()) ? SPACES.substring(0, count) : SPACES;
204     }
205 
startTag(final String format, final Object ... args)206     private void startTag(final String format, final Object ... args) {
207         Log.d(BUILDER_TAG, String.format(spaces(++mIndent * 2) + format, args));
208     }
209 
endTag(final String format, final Object ... args)210     private void endTag(final String format, final Object ... args) {
211         Log.d(BUILDER_TAG, String.format(spaces(mIndent-- * 2) + format, args));
212     }
213 
startEndTag(final String format, final Object ... args)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 
parseKeyboard(final XmlPullParser parser)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 
parseKeyboardAttributes(final XmlPullParser parser)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 
parseKeyboardContent(final XmlPullParser parser, final boolean skip)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 
parseRowAttributes(final XmlPullParser parser)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 
parseRowContent(final XmlPullParser parser, final KeyboardRow row, final boolean skip)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 
parseGridRows(final XmlPullParser parser, final boolean skip)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 
parseKey(final XmlPullParser parser, final KeyboardRow row, final boolean skip)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 
parseSpacer(final XmlPullParser parser, final KeyboardRow row, final boolean skip)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 
parseIncludeKeyboardContent(final XmlPullParser parser, final boolean skip)511     private void parseIncludeKeyboardContent(final XmlPullParser parser, final boolean skip)
512             throws XmlPullParserException, IOException {
513         parseIncludeInternal(parser, null, skip);
514     }
515 
parseIncludeRowContent(final XmlPullParser parser, final KeyboardRow row, final boolean skip)516     private void parseIncludeRowContent(final XmlPullParser parser, final KeyboardRow row,
517             final boolean skip) throws XmlPullParserException, IOException {
518         parseIncludeInternal(parser, row, skip);
519     }
520 
parseIncludeInternal(final XmlPullParser parser, final KeyboardRow row, final boolean skip)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 
parseMerge(final XmlPullParser parser, final KeyboardRow row, final boolean skip)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 
parseSwitchKeyboardContent(final XmlPullParser parser, final boolean skip)588     private void parseSwitchKeyboardContent(final XmlPullParser parser, final boolean skip)
589             throws XmlPullParserException, IOException {
590         parseSwitchInternal(parser, null, skip);
591     }
592 
parseSwitchRowContent(final XmlPullParser parser, final KeyboardRow row, final boolean skip)593     private void parseSwitchRowContent(final XmlPullParser parser, final KeyboardRow row,
594             final boolean skip) throws XmlPullParserException, IOException {
595         parseSwitchInternal(parser, row, skip);
596     }
597 
parseSwitchInternal(final XmlPullParser parser, final KeyboardRow row, final boolean skip)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 
parseCase(final XmlPullParser parser, final KeyboardRow row, final boolean skip)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 
parseCaseCondition(final XmlPullParser parser)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 
matchLocaleCodes(TypedArray caseAttr, final Locale locale)734     private static boolean matchLocaleCodes(TypedArray caseAttr, final Locale locale) {
735         return matchString(caseAttr, R.styleable.Keyboard_Case_localeCode, locale.toString());
736     }
737 
matchLanguageCodes(TypedArray caseAttr, Locale locale)738     private static boolean matchLanguageCodes(TypedArray caseAttr, Locale locale) {
739         return matchString(caseAttr, R.styleable.Keyboard_Case_languageCode, locale.getLanguage());
740     }
741 
matchCountryCodes(TypedArray caseAttr, Locale locale)742     private static boolean matchCountryCodes(TypedArray caseAttr, Locale locale) {
743         return matchString(caseAttr, R.styleable.Keyboard_Case_countryCode, locale.getCountry());
744     }
745 
matchInteger(final TypedArray a, final int index, final int value)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 
matchBoolean(final TypedArray a, final int index, final boolean value)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 
matchString(final TypedArray a, final int index, final String value)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 
matchTypedValue(final TypedArray a, final int index, final int intValue, final String strValue)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 
isIconDefined(final TypedArray a, final int index, final KeyboardIconsSet iconsSet)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 
parseDefault(final XmlPullParser parser, final KeyboardRow row, final boolean skip)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 
parseKeyStyle(final XmlPullParser parser, final boolean skip)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 
startKeyboard()829     private void startKeyboard() {
830         mCurrentY += mParams.mTopPadding;
831         mTopEdge = true;
832     }
833 
startRow(final KeyboardRow row)834     private void startRow(final KeyboardRow row) {
835         addEdgeSpace(mParams.mLeftPadding, row);
836         mCurrentRow = row;
837         mLeftEdge = true;
838         mRightEdgeKey = null;
839     }
840 
endRow(final KeyboardRow row)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 
endKey(@onnull final Key key)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 
endKeyboard()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 
addEdgeSpace(final float width, final KeyboardRow row)875     private void addEdgeSpace(final float width, final KeyboardRow row) {
876         row.advanceXPos(width);
877         mLeftEdge = false;
878         mRightEdgeKey = null;
879     }
880 
textAttr(final String value, final String name)881     private static String textAttr(final String value, final String name) {
882         return value != null ? String.format(" %s=%s", name, value) : "";
883     }
884 
booleanAttr(final TypedArray a, final int index, final String name)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