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