• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5  * use this file except in compliance with the License. You may obtain a copy of
6  * the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13  * License for the specific language governing permissions and limitations under
14  * the License.
15  */
16 
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.util.DisplayMetrics;
23 import android.util.Log;
24 import android.util.TypedValue;
25 import android.util.Xml;
26 import android.view.InflateException;
27 
28 import com.android.inputmethod.compat.EditorInfoCompatUtils;
29 import com.android.inputmethod.keyboard.Key;
30 import com.android.inputmethod.keyboard.Keyboard;
31 import com.android.inputmethod.keyboard.KeyboardId;
32 import com.android.inputmethod.latin.LatinImeLogger;
33 import com.android.inputmethod.latin.R;
34 
35 import org.xmlpull.v1.XmlPullParser;
36 import org.xmlpull.v1.XmlPullParserException;
37 
38 import java.io.IOException;
39 import java.util.Arrays;
40 
41 /**
42  * Keyboard Building helper.
43  *
44  * This class parses Keyboard XML file and eventually build a Keyboard.
45  * The Keyboard XML file looks like:
46  * <pre>
47  *   &gt;!-- xml/keyboard.xml --&lt;
48  *   &gt;Keyboard keyboard_attributes*&lt;
49  *     &gt;!-- Keyboard Content --&lt;
50  *     &gt;Row row_attributes*&lt;
51  *       &gt;!-- Row Content --&lt;
52  *       &gt;Key key_attributes* /&lt;
53  *       &gt;Spacer horizontalGap="0.2in" /&lt;
54  *       &gt;include keyboardLayout="@xml/other_keys"&lt;
55  *       ...
56  *     &gt;/Row&lt;
57  *     &gt;include keyboardLayout="@xml/other_rows"&lt;
58  *     ...
59  *   &gt;/Keyboard&lt;
60  * </pre>
61  * The XML file which is included in other file must have &gt;merge&lt; as root element, such as:
62  * <pre>
63  *   &gt;!-- xml/other_keys.xml --&lt;
64  *   &gt;merge&lt;
65  *     &gt;Key key_attributes* /&lt;
66  *     ...
67  *   &gt;/merge&lt;
68  * </pre>
69  * and
70  * <pre>
71  *   &gt;!-- xml/other_rows.xml --&lt;
72  *   &gt;merge&lt;
73  *     &gt;Row row_attributes*&lt;
74  *       &gt;Key key_attributes* /&lt;
75  *     &gt;/Row&lt;
76  *     ...
77  *   &gt;/merge&lt;
78  * </pre>
79  * You can also use switch-case-default tags to select Rows and Keys.
80  * <pre>
81  *   &gt;switch&lt;
82  *     &gt;case case_attribute*&lt;
83  *       &gt;!-- Any valid tags at switch position --&lt;
84  *     &gt;/case&lt;
85  *     ...
86  *     &gt;default&lt;
87  *       &gt;!-- Any valid tags at switch position --&lt;
88  *     &gt;/default&lt;
89  *   &gt;/switch&lt;
90  * </pre>
91  * You can declare Key style and specify styles within Key tags.
92  * <pre>
93  *     &gt;switch&lt;
94  *       &gt;case mode="email"&lt;
95  *         &gt;key-style styleName="f1-key" parentStyle="modifier-key"
96  *           keyLabel=".com"
97  *         /&lt;
98  *       &gt;/case&lt;
99  *       &gt;case mode="url"&lt;
100  *         &gt;key-style styleName="f1-key" parentStyle="modifier-key"
101  *           keyLabel="http://"
102  *         /&lt;
103  *       &gt;/case&lt;
104  *     &gt;/switch&lt;
105  *     ...
106  *     &gt;Key keyStyle="shift-key" ... /&lt;
107  * </pre>
108  */
109 
110 public class KeyboardBuilder<KP extends KeyboardParams> {
111     private static final String TAG = KeyboardBuilder.class.getSimpleName();
112     private static final boolean DEBUG = false;
113 
114     // Keyboard XML Tags
115     private static final String TAG_KEYBOARD = "Keyboard";
116     private static final String TAG_ROW = "Row";
117     private static final String TAG_KEY = "Key";
118     private static final String TAG_SPACER = "Spacer";
119     private static final String TAG_INCLUDE = "include";
120     private static final String TAG_MERGE = "merge";
121     private static final String TAG_SWITCH = "switch";
122     private static final String TAG_CASE = "case";
123     private static final String TAG_DEFAULT = "default";
124     public static final String TAG_KEY_STYLE = "key-style";
125 
126     private static final int DEFAULT_KEYBOARD_COLUMNS = 10;
127     private static final int DEFAULT_KEYBOARD_ROWS = 4;
128 
129     protected final KP mParams;
130     protected final Context mContext;
131     protected final Resources mResources;
132     private final DisplayMetrics mDisplayMetrics;
133 
134     private int mCurrentY = 0;
135     private Row mCurrentRow = null;
136     private boolean mLeftEdge;
137     private boolean mTopEdge;
138     private Key mRightEdgeKey = null;
139     private final KeyStyles mKeyStyles = new KeyStyles();
140 
141     /**
142      * Container for keys in the keyboard. All keys in a row are at the same Y-coordinate.
143      * Some of the key size defaults can be overridden per row from what the {@link Keyboard}
144      * defines.
145      */
146     public static class Row {
147         // keyWidth enum constants
148         private static final int KEYWIDTH_NOT_ENUM = 0;
149         private static final int KEYWIDTH_FILL_RIGHT = -1;
150         private static final int KEYWIDTH_FILL_BOTH = -2;
151 
152         private final KeyboardParams mParams;
153         /** Default width of a key in this row. */
154         public final float mDefaultKeyWidth;
155         /** Default height of a key in this row. */
156         public final int mRowHeight;
157 
158         private final int mCurrentY;
159         // Will be updated by {@link Key}'s constructor.
160         private float mCurrentX;
161 
Row(Resources res, KeyboardParams params, XmlPullParser parser, int y)162         public Row(Resources res, KeyboardParams params, XmlPullParser parser, int y) {
163             mParams = params;
164             TypedArray keyboardAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
165                     R.styleable.Keyboard);
166             mRowHeight = (int)KeyboardBuilder.getDimensionOrFraction(keyboardAttr,
167                     R.styleable.Keyboard_rowHeight, params.mBaseHeight, params.mDefaultRowHeight);
168             keyboardAttr.recycle();
169             TypedArray keyAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
170                     R.styleable.Keyboard_Key);
171             mDefaultKeyWidth = KeyboardBuilder.getDimensionOrFraction(keyboardAttr,
172                     R.styleable.Keyboard_Key_keyWidth, params.mBaseWidth, params.mDefaultKeyWidth);
173             keyAttr.recycle();
174 
175             mCurrentY = y;
176             mCurrentX = 0.0f;
177         }
178 
setXPos(float keyXPos)179         public void setXPos(float keyXPos) {
180             mCurrentX = keyXPos;
181         }
182 
advanceXPos(float width)183         public void advanceXPos(float width) {
184             mCurrentX += width;
185         }
186 
getKeyY()187         public int getKeyY() {
188             return mCurrentY;
189         }
190 
getKeyX(TypedArray keyAttr)191         public float getKeyX(TypedArray keyAttr) {
192             final int widthType = KeyboardBuilder.getEnumValue(keyAttr,
193                     R.styleable.Keyboard_Key_keyWidth, KEYWIDTH_NOT_ENUM);
194             if (widthType == KEYWIDTH_FILL_BOTH) {
195                 // If keyWidth is fillBoth, the key width should start right after the nearest key
196                 // on the left hand side.
197                 return mCurrentX;
198             }
199 
200             final int keyboardRightEdge = mParams.mOccupiedWidth - mParams.mHorizontalEdgesPadding;
201             if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyXPos)) {
202                 final float keyXPos = KeyboardBuilder.getDimensionOrFraction(keyAttr,
203                         R.styleable.Keyboard_Key_keyXPos, mParams.mBaseWidth, 0);
204                 if (keyXPos < 0) {
205                     // If keyXPos is negative, the actual x-coordinate will be
206                     // keyboardWidth + keyXPos.
207                     // keyXPos shouldn't be less than mCurrentX because drawable area for this key
208                     // starts at mCurrentX. Or, this key will overlaps the adjacent key on its left
209                     // hand side.
210                     return Math.max(keyXPos + keyboardRightEdge, mCurrentX);
211                 } else {
212                     return keyXPos + mParams.mHorizontalEdgesPadding;
213                 }
214             }
215             return mCurrentX;
216         }
217 
getKeyWidth(TypedArray keyAttr, float keyXPos)218         public float getKeyWidth(TypedArray keyAttr, float keyXPos) {
219             final int widthType = KeyboardBuilder.getEnumValue(keyAttr,
220                     R.styleable.Keyboard_Key_keyWidth, KEYWIDTH_NOT_ENUM);
221             switch (widthType) {
222             case KEYWIDTH_FILL_RIGHT:
223             case KEYWIDTH_FILL_BOTH:
224                 final int keyboardRightEdge =
225                         mParams.mOccupiedWidth - mParams.mHorizontalEdgesPadding;
226                 // If keyWidth is fillRight, the actual key width will be determined to fill out the
227                 // area up to the right edge of the keyboard.
228                 // If keyWidth is fillBoth, the actual key width will be determined to fill out the
229                 // area between the nearest key on the left hand side and the right edge of the
230                 // keyboard.
231                 return keyboardRightEdge - keyXPos;
232             default: // KEYWIDTH_NOT_ENUM
233                 return KeyboardBuilder.getDimensionOrFraction(keyAttr,
234                         R.styleable.Keyboard_Key_keyWidth, mParams.mBaseWidth, mDefaultKeyWidth);
235             }
236         }
237     }
238 
KeyboardBuilder(Context context, KP params)239     public KeyboardBuilder(Context context, KP params) {
240         mContext = context;
241         final Resources res = context.getResources();
242         mResources = res;
243         mDisplayMetrics = res.getDisplayMetrics();
244 
245         mParams = params;
246 
247         setTouchPositionCorrectionData(context, params);
248 
249         params.GRID_WIDTH = res.getInteger(R.integer.config_keyboard_grid_width);
250         params.GRID_HEIGHT = res.getInteger(R.integer.config_keyboard_grid_height);
251     }
252 
setTouchPositionCorrectionData(Context context, KeyboardParams params)253     private static void setTouchPositionCorrectionData(Context context, KeyboardParams params) {
254         final TypedArray a = context.obtainStyledAttributes(
255                 null, R.styleable.Keyboard, R.attr.keyboardStyle, 0);
256         params.mThemeId = a.getInt(R.styleable.Keyboard_themeId, 0);
257         final int resourceId = a.getResourceId(R.styleable.Keyboard_touchPositionCorrectionData, 0);
258         a.recycle();
259         if (resourceId == 0) {
260             if (LatinImeLogger.sDBG)
261                 throw new RuntimeException("touchPositionCorrectionData is not defined");
262             return;
263         }
264 
265         final String[] data = context.getResources().getStringArray(resourceId);
266         params.mTouchPositionCorrection.load(data);
267     }
268 
load(KeyboardId id)269     public KeyboardBuilder<KP> load(KeyboardId id) {
270         mParams.mId = id;
271         try {
272             parseKeyboard(id.getXmlId());
273         } catch (XmlPullParserException e) {
274             Log.w(TAG, "keyboard XML parse error: " + e);
275             throw new IllegalArgumentException(e);
276         } catch (IOException e) {
277             Log.w(TAG, "keyboard XML parse error: " + e);
278             throw new RuntimeException(e);
279         }
280         return this;
281     }
282 
setTouchPositionCorrectionEnabled(boolean enabled)283     public void setTouchPositionCorrectionEnabled(boolean enabled) {
284         mParams.mTouchPositionCorrection.setEnabled(enabled);
285     }
286 
build()287     public Keyboard build() {
288         return new Keyboard(mParams);
289     }
290 
parseKeyboard(int resId)291     private void parseKeyboard(int resId) throws XmlPullParserException, IOException {
292         if (DEBUG) Log.d(TAG, String.format("<%s> %s", TAG_KEYBOARD, mParams.mId));
293         final XmlPullParser parser = mResources.getXml(resId);
294         int event;
295         while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
296             if (event == XmlPullParser.START_TAG) {
297                 final String tag = parser.getName();
298                 if (TAG_KEYBOARD.equals(tag)) {
299                     parseKeyboardAttributes(parser);
300                     startKeyboard();
301                     parseKeyboardContent(parser, false);
302                     break;
303                 } else {
304                     throw new IllegalStartTag(parser, TAG_KEYBOARD);
305                 }
306             }
307         }
308     }
309 
parseKeyboardLocale( Context context, int resId)310     public static String parseKeyboardLocale(
311             Context context, int resId) throws XmlPullParserException, IOException {
312         final Resources res = context.getResources();
313         final XmlPullParser parser = res.getXml(resId);
314         if (parser == null) return "";
315         int event;
316         while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
317             if (event == XmlPullParser.START_TAG) {
318                 final String tag = parser.getName();
319                 if (TAG_KEYBOARD.equals(tag)) {
320                     final TypedArray keyboardAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
321                             R.styleable.Keyboard);
322                     return keyboardAttr.getString(R.styleable.Keyboard_keyboardLocale);
323                 } else {
324                     throw new IllegalStartTag(parser, TAG_KEYBOARD);
325                 }
326             }
327         }
328         return "";
329     }
330 
parseKeyboardAttributes(XmlPullParser parser)331     private void parseKeyboardAttributes(XmlPullParser parser) {
332         final int displayWidth = mDisplayMetrics.widthPixels;
333         final TypedArray keyboardAttr = mContext.obtainStyledAttributes(
334                 Xml.asAttributeSet(parser), R.styleable.Keyboard, R.attr.keyboardStyle,
335                 R.style.Keyboard);
336         final TypedArray keyAttr = mResources.obtainAttributes(Xml.asAttributeSet(parser),
337                 R.styleable.Keyboard_Key);
338         try {
339             final int displayHeight = mDisplayMetrics.heightPixels;
340             final int keyboardHeight = (int)keyboardAttr.getDimension(
341                     R.styleable.Keyboard_keyboardHeight, displayHeight / 2);
342             final int maxKeyboardHeight = (int)getDimensionOrFraction(keyboardAttr,
343                     R.styleable.Keyboard_maxKeyboardHeight, displayHeight, displayHeight / 2);
344             int minKeyboardHeight = (int)getDimensionOrFraction(keyboardAttr,
345                     R.styleable.Keyboard_minKeyboardHeight, displayHeight, displayHeight / 2);
346             if (minKeyboardHeight < 0) {
347                 // Specified fraction was negative, so it should be calculated against display
348                 // width.
349                 minKeyboardHeight = -(int)getDimensionOrFraction(keyboardAttr,
350                         R.styleable.Keyboard_minKeyboardHeight, displayWidth, displayWidth / 2);
351             }
352             final KeyboardParams params = mParams;
353             // Keyboard height will not exceed maxKeyboardHeight and will not be less than
354             // minKeyboardHeight.
355             params.mOccupiedHeight = Math.max(
356                     Math.min(keyboardHeight, maxKeyboardHeight), minKeyboardHeight);
357             params.mOccupiedWidth = params.mId.mWidth;
358             params.mTopPadding = (int)getDimensionOrFraction(keyboardAttr,
359                     R.styleable.Keyboard_keyboardTopPadding, params.mOccupiedHeight, 0);
360             params.mBottomPadding = (int)getDimensionOrFraction(keyboardAttr,
361                     R.styleable.Keyboard_keyboardBottomPadding, params.mOccupiedHeight, 0);
362             params.mHorizontalEdgesPadding = (int)getDimensionOrFraction(keyboardAttr,
363                     R.styleable.Keyboard_keyboardHorizontalEdgesPadding, mParams.mOccupiedWidth, 0);
364 
365             params.mBaseWidth = params.mOccupiedWidth - params.mHorizontalEdgesPadding * 2
366                     - params.mHorizontalCenterPadding;
367             params.mDefaultKeyWidth = (int)getDimensionOrFraction(keyAttr,
368                     R.styleable.Keyboard_Key_keyWidth, params.mBaseWidth,
369                     params.mBaseWidth / DEFAULT_KEYBOARD_COLUMNS);
370             params.mHorizontalGap = (int)getDimensionOrFraction(keyboardAttr,
371                     R.styleable.Keyboard_horizontalGap, params.mBaseWidth, 0);
372             params.mVerticalGap = (int)getDimensionOrFraction(keyboardAttr,
373                     R.styleable.Keyboard_verticalGap, params.mOccupiedHeight, 0);
374             params.mBaseHeight = params.mOccupiedHeight - params.mTopPadding
375                     - params.mBottomPadding + params.mVerticalGap;
376             params.mDefaultRowHeight = (int)getDimensionOrFraction(keyboardAttr,
377                     R.styleable.Keyboard_rowHeight, params.mBaseHeight,
378                     params.mBaseHeight / DEFAULT_KEYBOARD_ROWS);
379 
380             params.mIsRtlKeyboard = keyboardAttr.getBoolean(
381                     R.styleable.Keyboard_isRtlKeyboard, false);
382             params.mMoreKeysTemplate = keyboardAttr.getResourceId(
383                     R.styleable.Keyboard_moreKeysTemplate, 0);
384             params.mMaxMiniKeyboardColumn = keyAttr.getInt(
385                     R.styleable.Keyboard_Key_maxMoreKeysColumn, 5);
386 
387             params.mIconsSet.loadIcons(keyboardAttr);
388         } finally {
389             keyAttr.recycle();
390             keyboardAttr.recycle();
391         }
392     }
393 
parseKeyboardContent(XmlPullParser parser, boolean skip)394     private void parseKeyboardContent(XmlPullParser parser, boolean skip)
395             throws XmlPullParserException, IOException {
396         int event;
397         while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
398             if (event == XmlPullParser.START_TAG) {
399                 final String tag = parser.getName();
400                 if (TAG_ROW.equals(tag)) {
401                     Row row = parseRowAttributes(parser);
402                     if (DEBUG) Log.d(TAG, String.format("<%s>", TAG_ROW));
403                     if (!skip)
404                         startRow(row);
405                     parseRowContent(parser, row, skip);
406                 } else if (TAG_INCLUDE.equals(tag)) {
407                     parseIncludeKeyboardContent(parser, skip);
408                 } else if (TAG_SWITCH.equals(tag)) {
409                     parseSwitchKeyboardContent(parser, skip);
410                 } else if (TAG_KEY_STYLE.equals(tag)) {
411                     parseKeyStyle(parser, skip);
412                 } else {
413                     throw new IllegalStartTag(parser, TAG_ROW);
414                 }
415             } else if (event == XmlPullParser.END_TAG) {
416                 final String tag = parser.getName();
417                 if (TAG_KEYBOARD.equals(tag)) {
418                     endKeyboard();
419                     break;
420                 } else if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag)
421                         || TAG_MERGE.equals(tag)) {
422                     if (DEBUG) Log.d(TAG, String.format("</%s>", tag));
423                     break;
424                 } else if (TAG_KEY_STYLE.equals(tag)) {
425                     continue;
426                 } else {
427                     throw new IllegalEndTag(parser, TAG_ROW);
428                 }
429             }
430         }
431     }
432 
parseRowAttributes(XmlPullParser parser)433     private Row parseRowAttributes(XmlPullParser parser) {
434         final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
435                 R.styleable.Keyboard);
436         try {
437             if (a.hasValue(R.styleable.Keyboard_horizontalGap))
438                 throw new IllegalAttribute(parser, "horizontalGap");
439             if (a.hasValue(R.styleable.Keyboard_verticalGap))
440                 throw new IllegalAttribute(parser, "verticalGap");
441             return new Row(mResources, mParams, parser, mCurrentY);
442         } finally {
443             a.recycle();
444         }
445     }
446 
parseRowContent(XmlPullParser parser, Row row, boolean skip)447     private void parseRowContent(XmlPullParser parser, Row row, boolean skip)
448             throws XmlPullParserException, IOException {
449         int event;
450         while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
451             if (event == XmlPullParser.START_TAG) {
452                 final String tag = parser.getName();
453                 if (TAG_KEY.equals(tag)) {
454                     parseKey(parser, row, skip);
455                 } else if (TAG_SPACER.equals(tag)) {
456                     parseSpacer(parser, row, skip);
457                 } else if (TAG_INCLUDE.equals(tag)) {
458                     parseIncludeRowContent(parser, row, skip);
459                 } else if (TAG_SWITCH.equals(tag)) {
460                     parseSwitchRowContent(parser, row, skip);
461                 } else if (TAG_KEY_STYLE.equals(tag)) {
462                     parseKeyStyle(parser, skip);
463                 } else {
464                     throw new IllegalStartTag(parser, TAG_KEY);
465                 }
466             } else if (event == XmlPullParser.END_TAG) {
467                 final String tag = parser.getName();
468                 if (TAG_ROW.equals(tag)) {
469                     if (DEBUG) Log.d(TAG, String.format("</%s>", TAG_ROW));
470                     if (!skip)
471                         endRow(row);
472                     break;
473                 } else if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag)
474                         || TAG_MERGE.equals(tag)) {
475                     if (DEBUG) Log.d(TAG, String.format("</%s>", tag));
476                     break;
477                 } else if (TAG_KEY_STYLE.equals(tag)) {
478                     continue;
479                 } else {
480                     throw new IllegalEndTag(parser, TAG_KEY);
481                 }
482             }
483         }
484     }
485 
parseKey(XmlPullParser parser, Row row, boolean skip)486     private void parseKey(XmlPullParser parser, Row row, boolean skip)
487             throws XmlPullParserException, IOException {
488         if (skip) {
489             checkEndTag(TAG_KEY, parser);
490         } else {
491             final Key key = new Key(mResources, mParams, row, parser, mKeyStyles);
492             if (DEBUG) Log.d(TAG, String.format("<%s%s keyLabel=%s code=%d moreKeys=%s />",
493                     TAG_KEY, (key.isEnabled() ? "" : " disabled"), key.mLabel, key.mCode,
494                     Arrays.toString(key.mMoreKeys)));
495             checkEndTag(TAG_KEY, parser);
496             endKey(key);
497         }
498     }
499 
parseSpacer(XmlPullParser parser, Row row, boolean skip)500     private void parseSpacer(XmlPullParser parser, Row row, boolean skip)
501             throws XmlPullParserException, IOException {
502         if (skip) {
503             checkEndTag(TAG_SPACER, parser);
504         } else {
505             final Key.Spacer spacer = new Key.Spacer(mResources, mParams, row, parser, mKeyStyles);
506             if (DEBUG) Log.d(TAG, String.format("<%s />", TAG_SPACER));
507             checkEndTag(TAG_SPACER, parser);
508             endKey(spacer);
509         }
510     }
511 
parseIncludeKeyboardContent(XmlPullParser parser, boolean skip)512     private void parseIncludeKeyboardContent(XmlPullParser parser, boolean skip)
513             throws XmlPullParserException, IOException {
514         parseIncludeInternal(parser, null, skip);
515     }
516 
parseIncludeRowContent(XmlPullParser parser, Row row, boolean skip)517     private void parseIncludeRowContent(XmlPullParser parser, Row row, boolean skip)
518             throws XmlPullParserException, IOException {
519         parseIncludeInternal(parser, row, skip);
520     }
521 
parseIncludeInternal(XmlPullParser parser, Row row, boolean skip)522     private void parseIncludeInternal(XmlPullParser parser, Row row, boolean skip)
523             throws XmlPullParserException, IOException {
524         if (skip) {
525             checkEndTag(TAG_INCLUDE, parser);
526         } else {
527             final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
528                     R.styleable.Keyboard_Include);
529             final int keyboardLayout = a.getResourceId(
530                     R.styleable.Keyboard_Include_keyboardLayout, 0);
531             a.recycle();
532 
533             checkEndTag(TAG_INCLUDE, parser);
534             if (keyboardLayout == 0)
535                 throw new ParseException("No keyboardLayout attribute in <include/>", parser);
536             if (DEBUG) Log.d(TAG, String.format("<%s keyboardLayout=%s />",
537                     TAG_INCLUDE, mResources.getResourceEntryName(keyboardLayout)));
538             parseMerge(mResources.getLayout(keyboardLayout), row, skip);
539         }
540     }
541 
parseMerge(XmlPullParser parser, Row row, boolean skip)542     private void parseMerge(XmlPullParser parser, Row row, boolean skip)
543             throws XmlPullParserException, IOException {
544         int event;
545         while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
546             if (event == XmlPullParser.START_TAG) {
547                 final String tag = parser.getName();
548                 if (TAG_MERGE.equals(tag)) {
549                     if (row == null) {
550                         parseKeyboardContent(parser, skip);
551                     } else {
552                         parseRowContent(parser, row, skip);
553                     }
554                     break;
555                 } else {
556                     throw new ParseException(
557                             "Included keyboard layout must have <merge> root element", parser);
558                 }
559             }
560         }
561     }
562 
parseSwitchKeyboardContent(XmlPullParser parser, boolean skip)563     private void parseSwitchKeyboardContent(XmlPullParser parser, boolean skip)
564             throws XmlPullParserException, IOException {
565         parseSwitchInternal(parser, null, skip);
566     }
567 
parseSwitchRowContent(XmlPullParser parser, Row row, boolean skip)568     private void parseSwitchRowContent(XmlPullParser parser, Row row, boolean skip)
569             throws XmlPullParserException, IOException {
570         parseSwitchInternal(parser, row, skip);
571     }
572 
parseSwitchInternal(XmlPullParser parser, Row row, boolean skip)573     private void parseSwitchInternal(XmlPullParser parser, Row row, boolean skip)
574             throws XmlPullParserException, IOException {
575         if (DEBUG) Log.d(TAG, String.format("<%s> %s", TAG_SWITCH, mParams.mId));
576         boolean selected = false;
577         int event;
578         while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
579             if (event == XmlPullParser.START_TAG) {
580                 final String tag = parser.getName();
581                 if (TAG_CASE.equals(tag)) {
582                     selected |= parseCase(parser, row, selected ? true : skip);
583                 } else if (TAG_DEFAULT.equals(tag)) {
584                     selected |= parseDefault(parser, row, selected ? true : skip);
585                 } else {
586                     throw new IllegalStartTag(parser, TAG_KEY);
587                 }
588             } else if (event == XmlPullParser.END_TAG) {
589                 final String tag = parser.getName();
590                 if (TAG_SWITCH.equals(tag)) {
591                     if (DEBUG) Log.d(TAG, String.format("</%s>", TAG_SWITCH));
592                     break;
593                 } else {
594                     throw new IllegalEndTag(parser, TAG_KEY);
595                 }
596             }
597         }
598     }
599 
parseCase(XmlPullParser parser, Row row, boolean skip)600     private boolean parseCase(XmlPullParser parser, Row row, boolean skip)
601             throws XmlPullParserException, IOException {
602         final boolean selected = parseCaseCondition(parser);
603         if (row == null) {
604             // Processing Rows.
605             parseKeyboardContent(parser, selected ? skip : true);
606         } else {
607             // Processing Keys.
608             parseRowContent(parser, row, selected ? skip : true);
609         }
610         return selected;
611     }
612 
parseCaseCondition(XmlPullParser parser)613     private boolean parseCaseCondition(XmlPullParser parser) {
614         final KeyboardId id = mParams.mId;
615         if (id == null)
616             return true;
617 
618         final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
619                 R.styleable.Keyboard_Case);
620         try {
621             final boolean modeMatched = matchTypedValue(a,
622                     R.styleable.Keyboard_Case_mode, id.mMode, KeyboardId.modeName(id.mMode));
623             final boolean navigateActionMatched = matchBoolean(a,
624                     R.styleable.Keyboard_Case_navigateAction, id.mNavigateAction);
625             final boolean passwordInputMatched = matchBoolean(a,
626                     R.styleable.Keyboard_Case_passwordInput, id.mPasswordInput);
627             final boolean hasSettingsKeyMatched = matchBoolean(a,
628                     R.styleable.Keyboard_Case_hasSettingsKey, id.mHasSettingsKey);
629             final boolean f2KeyModeMatched = matchInteger(a,
630                     R.styleable.Keyboard_Case_f2KeyMode, id.mF2KeyMode);
631             final boolean clobberSettingsKeyMatched = matchBoolean(a,
632                     R.styleable.Keyboard_Case_clobberSettingsKey, id.mClobberSettingsKey);
633             final boolean shortcutKeyEnabledMatched = matchBoolean(a,
634                     R.styleable.Keyboard_Case_shortcutKeyEnabled, id.mShortcutKeyEnabled);
635             final boolean hasShortcutKeyMatched = matchBoolean(a,
636                     R.styleable.Keyboard_Case_hasShortcutKey, id.mHasShortcutKey);
637             // As noted at {@link KeyboardId} class, we are interested only in enum value masked by
638             // {@link android.view.inputmethod.EditorInfo#IME_MASK_ACTION} and
639             // {@link android.view.inputmethod.EditorInfo#IME_FLAG_NO_ENTER_ACTION}. So matching
640             // this attribute with id.mImeOptions as integer value is enough for our purpose.
641             final boolean imeActionMatched = matchInteger(a,
642                     R.styleable.Keyboard_Case_imeAction, id.mImeAction);
643             final boolean localeCodeMatched = matchString(a,
644                     R.styleable.Keyboard_Case_localeCode, id.mLocale.toString());
645             final boolean languageCodeMatched = matchString(a,
646                     R.styleable.Keyboard_Case_languageCode, id.mLocale.getLanguage());
647             final boolean countryCodeMatched = matchString(a,
648                     R.styleable.Keyboard_Case_countryCode, id.mLocale.getCountry());
649             final boolean selected = modeMatched && navigateActionMatched && passwordInputMatched
650                     && hasSettingsKeyMatched && f2KeyModeMatched && clobberSettingsKeyMatched
651                     && shortcutKeyEnabledMatched && hasShortcutKeyMatched && imeActionMatched &&
652                     localeCodeMatched && languageCodeMatched && countryCodeMatched;
653 
654             if (DEBUG) Log.d(TAG, String.format("<%s%s%s%s%s%s%s%s%s%s%s%s%s> %s", TAG_CASE,
655                     textAttr(a.getString(R.styleable.Keyboard_Case_mode), "mode"),
656                     booleanAttr(a, R.styleable.Keyboard_Case_navigateAction, "navigateAction"),
657                     booleanAttr(a, R.styleable.Keyboard_Case_passwordInput, "passwordInput"),
658                     booleanAttr(a, R.styleable.Keyboard_Case_hasSettingsKey, "hasSettingsKey"),
659                     textAttr(KeyboardId.f2KeyModeName(
660                             a.getInt(R.styleable.Keyboard_Case_f2KeyMode, -1)), "f2KeyMode"),
661                     booleanAttr(a, R.styleable.Keyboard_Case_clobberSettingsKey,
662                             "clobberSettingsKey"),
663                     booleanAttr(
664                             a, R.styleable.Keyboard_Case_shortcutKeyEnabled, "shortcutKeyEnabled"),
665                     booleanAttr(a, R.styleable.Keyboard_Case_hasShortcutKey, "hasShortcutKey"),
666                     textAttr(EditorInfoCompatUtils.imeOptionsName(
667                             a.getInt(R.styleable.Keyboard_Case_imeAction, -1)), "imeAction"),
668                     textAttr(a.getString(R.styleable.Keyboard_Case_localeCode), "localeCode"),
669                     textAttr(a.getString(R.styleable.Keyboard_Case_languageCode), "languageCode"),
670                     textAttr(a.getString(R.styleable.Keyboard_Case_countryCode), "countryCode"),
671                     Boolean.toString(selected)));
672 
673             return selected;
674         } finally {
675             a.recycle();
676         }
677     }
678 
matchInteger(TypedArray a, int index, int value)679     private static boolean matchInteger(TypedArray a, int index, int value) {
680         // If <case> does not have "index" attribute, that means this <case> is wild-card for the
681         // attribute.
682         return !a.hasValue(index) || a.getInt(index, 0) == value;
683     }
684 
matchBoolean(TypedArray a, int index, boolean value)685     private static boolean matchBoolean(TypedArray a, int index, boolean value) {
686         // If <case> does not have "index" attribute, that means this <case> is wild-card for the
687         // attribute.
688         return !a.hasValue(index) || a.getBoolean(index, false) == value;
689     }
690 
matchString(TypedArray a, int index, String value)691     private static boolean matchString(TypedArray a, int index, String value) {
692         // If <case> does not have "index" attribute, that means this <case> is wild-card for the
693         // attribute.
694         return !a.hasValue(index) || stringArrayContains(a.getString(index).split("\\|"), value);
695     }
696 
matchTypedValue(TypedArray a, int index, int intValue, String strValue)697     private static boolean matchTypedValue(TypedArray a, int index, int intValue, String strValue) {
698         // If <case> does not have "index" attribute, that means this <case> is wild-card for the
699         // attribute.
700         final TypedValue v = a.peekValue(index);
701         if (v == null)
702             return true;
703 
704         if (isIntegerValue(v)) {
705             return intValue == a.getInt(index, 0);
706         } else if (isStringValue(v)) {
707             return stringArrayContains(a.getString(index).split("\\|"), strValue);
708         }
709         return false;
710     }
711 
stringArrayContains(String[] array, String value)712     private static boolean stringArrayContains(String[] array, String value) {
713         for (final String elem : array) {
714             if (elem.equals(value))
715                 return true;
716         }
717         return false;
718     }
719 
parseDefault(XmlPullParser parser, Row row, boolean skip)720     private boolean parseDefault(XmlPullParser parser, Row row, boolean skip)
721             throws XmlPullParserException, IOException {
722         if (DEBUG) Log.d(TAG, String.format("<%s>", TAG_DEFAULT));
723         if (row == null) {
724             parseKeyboardContent(parser, skip);
725         } else {
726             parseRowContent(parser, row, skip);
727         }
728         return true;
729     }
730 
parseKeyStyle(XmlPullParser parser, boolean skip)731     private void parseKeyStyle(XmlPullParser parser, boolean skip) {
732         TypedArray keyStyleAttr = mResources.obtainAttributes(Xml.asAttributeSet(parser),
733                 R.styleable.Keyboard_KeyStyle);
734         TypedArray keyAttrs = mResources.obtainAttributes(Xml.asAttributeSet(parser),
735                 R.styleable.Keyboard_Key);
736         try {
737             if (!keyStyleAttr.hasValue(R.styleable.Keyboard_KeyStyle_styleName))
738                 throw new ParseException("<" + TAG_KEY_STYLE
739                         + "/> needs styleName attribute", parser);
740             if (!skip)
741                 mKeyStyles.parseKeyStyleAttributes(keyStyleAttr, keyAttrs, parser);
742         } finally {
743             keyStyleAttr.recycle();
744             keyAttrs.recycle();
745         }
746     }
747 
checkEndTag(String tag, XmlPullParser parser)748     private static void checkEndTag(String tag, XmlPullParser parser)
749             throws XmlPullParserException, IOException {
750         if (parser.next() == XmlPullParser.END_TAG && tag.equals(parser.getName()))
751             return;
752         throw new NonEmptyTag(tag, parser);
753     }
754 
startKeyboard()755     private void startKeyboard() {
756         mCurrentY += mParams.mTopPadding;
757         mTopEdge = true;
758     }
759 
startRow(Row row)760     private void startRow(Row row) {
761         addEdgeSpace(mParams.mHorizontalEdgesPadding, row);
762         mCurrentRow = row;
763         mLeftEdge = true;
764         mRightEdgeKey = null;
765     }
766 
endRow(Row row)767     private void endRow(Row row) {
768         if (mCurrentRow == null)
769             throw new InflateException("orphant end row tag");
770         if (mRightEdgeKey != null) {
771             mRightEdgeKey.markAsRightEdge(mParams);
772             mRightEdgeKey = null;
773         }
774         addEdgeSpace(mParams.mHorizontalEdgesPadding, row);
775         mCurrentY += row.mRowHeight;
776         mCurrentRow = null;
777         mTopEdge = false;
778     }
779 
endKey(Key key)780     private void endKey(Key key) {
781         mParams.onAddKey(key);
782         if (mLeftEdge) {
783             key.markAsLeftEdge(mParams);
784             mLeftEdge = false;
785         }
786         if (mTopEdge) {
787             key.markAsTopEdge(mParams);
788         }
789         mRightEdgeKey = key;
790     }
791 
endKeyboard()792     private void endKeyboard() {
793     }
794 
addEdgeSpace(float width, Row row)795     private void addEdgeSpace(float width, Row row) {
796         row.advanceXPos(width);
797         mLeftEdge = false;
798         mRightEdgeKey = null;
799     }
800 
getDimensionOrFraction(TypedArray a, int index, int base, float defValue)801     public static float getDimensionOrFraction(TypedArray a, int index, int base, float defValue) {
802         final TypedValue value = a.peekValue(index);
803         if (value == null)
804             return defValue;
805         if (isFractionValue(value)) {
806             return a.getFraction(index, base, base, defValue);
807         } else if (isDimensionValue(value)) {
808             return a.getDimension(index, defValue);
809         }
810         return defValue;
811     }
812 
getEnumValue(TypedArray a, int index, int defValue)813     public static int getEnumValue(TypedArray a, int index, int defValue) {
814         final TypedValue value = a.peekValue(index);
815         if (value == null)
816             return defValue;
817         if (isIntegerValue(value)) {
818             return a.getInt(index, defValue);
819         }
820         return defValue;
821     }
822 
isFractionValue(TypedValue v)823     private static boolean isFractionValue(TypedValue v) {
824         return v.type == TypedValue.TYPE_FRACTION;
825     }
826 
isDimensionValue(TypedValue v)827     private static boolean isDimensionValue(TypedValue v) {
828         return v.type == TypedValue.TYPE_DIMENSION;
829     }
830 
isIntegerValue(TypedValue v)831     private static boolean isIntegerValue(TypedValue v) {
832         return v.type >= TypedValue.TYPE_FIRST_INT && v.type <= TypedValue.TYPE_LAST_INT;
833     }
834 
isStringValue(TypedValue v)835     private static boolean isStringValue(TypedValue v) {
836         return v.type == TypedValue.TYPE_STRING;
837     }
838 
839     @SuppressWarnings("serial")
840     public static class ParseException extends InflateException {
ParseException(String msg, XmlPullParser parser)841         public ParseException(String msg, XmlPullParser parser) {
842             super(msg + " at line " + parser.getLineNumber());
843         }
844     }
845 
846     @SuppressWarnings("serial")
847     private static class IllegalStartTag extends ParseException {
IllegalStartTag(XmlPullParser parser, String parent)848         public IllegalStartTag(XmlPullParser parser, String parent) {
849             super("Illegal start tag " + parser.getName() + " in " + parent, parser);
850         }
851     }
852 
853     @SuppressWarnings("serial")
854     private static class IllegalEndTag extends ParseException {
IllegalEndTag(XmlPullParser parser, String parent)855         public IllegalEndTag(XmlPullParser parser, String parent) {
856             super("Illegal end tag " + parser.getName() + " in " + parent, parser);
857         }
858     }
859 
860     @SuppressWarnings("serial")
861     private static class IllegalAttribute extends ParseException {
IllegalAttribute(XmlPullParser parser, String attribute)862         public IllegalAttribute(XmlPullParser parser, String attribute) {
863             super("Tag " + parser.getName() + " has illegal attribute " + attribute, parser);
864         }
865     }
866 
867     @SuppressWarnings("serial")
868     private static class NonEmptyTag extends ParseException {
NonEmptyTag(String tag, XmlPullParser parser)869         public NonEmptyTag(String tag, XmlPullParser parser) {
870             super(tag + " must be empty tag", parser);
871         }
872     }
873 
textAttr(String value, String name)874     private static String textAttr(String value, String name) {
875         return value != null ? String.format(" %s=%s", name, value) : "";
876     }
877 
booleanAttr(TypedArray a, int index, String name)878     private static String booleanAttr(TypedArray a, int index, String name) {
879         return a.hasValue(index) ? String.format(" %s=%s", name, a.getBoolean(index, false)) : "";
880     }
881 }
882