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