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