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