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