1 /* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of 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, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.inputmethod.keyboard.internal; 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.Log; 25 import android.util.TypedValue; 26 import android.util.Xml; 27 28 import com.android.inputmethod.annotations.UsedForTesting; 29 import com.android.inputmethod.keyboard.Key; 30 import com.android.inputmethod.keyboard.Keyboard; 31 import com.android.inputmethod.keyboard.KeyboardId; 32 import com.android.inputmethod.latin.LocaleUtils.RunInLocale; 33 import com.android.inputmethod.latin.R; 34 import com.android.inputmethod.latin.ResourceUtils; 35 import com.android.inputmethod.latin.StringUtils; 36 import com.android.inputmethod.latin.SubtypeLocale; 37 import com.android.inputmethod.latin.XmlParseUtils; 38 39 import org.xmlpull.v1.XmlPullParser; 40 import org.xmlpull.v1.XmlPullParserException; 41 42 import java.io.IOException; 43 import java.util.Arrays; 44 import java.util.Locale; 45 46 /** 47 * Keyboard Building helper. 48 * 49 * This class parses Keyboard XML file and eventually build a Keyboard. 50 * The Keyboard XML file looks like: 51 * <pre> 52 * <!-- xml/keyboard.xml --> 53 * <Keyboard keyboard_attributes*> 54 * <!-- Keyboard Content --> 55 * <Row row_attributes*> 56 * <!-- Row Content --> 57 * <Key key_attributes* /> 58 * <Spacer horizontalGap="32.0dp" /> 59 * <include keyboardLayout="@xml/other_keys"> 60 * ... 61 * </Row> 62 * <include keyboardLayout="@xml/other_rows"> 63 * ... 64 * </Keyboard> 65 * </pre> 66 * The XML file which is included in other file must have <merge> as root element, 67 * such as: 68 * <pre> 69 * <!-- xml/other_keys.xml --> 70 * <merge> 71 * <Key key_attributes* /> 72 * ... 73 * </merge> 74 * </pre> 75 * and 76 * <pre> 77 * <!-- xml/other_rows.xml --> 78 * <merge> 79 * <Row row_attributes*> 80 * <Key key_attributes* /> 81 * </Row> 82 * ... 83 * </merge> 84 * </pre> 85 * You can also use switch-case-default tags to select Rows and Keys. 86 * <pre> 87 * <switch> 88 * <case case_attribute*> 89 * <!-- Any valid tags at switch position --> 90 * </case> 91 * ... 92 * <default> 93 * <!-- Any valid tags at switch position --> 94 * </default> 95 * </switch> 96 * </pre> 97 * You can declare Key style and specify styles within Key tags. 98 * <pre> 99 * <switch> 100 * <case mode="email"> 101 * <key-style styleName="f1-key" parentStyle="modifier-key" 102 * keyLabel=".com" 103 * /> 104 * </case> 105 * <case mode="url"> 106 * <key-style styleName="f1-key" parentStyle="modifier-key" 107 * keyLabel="http://" 108 * /> 109 * </case> 110 * </switch> 111 * ... 112 * <Key keyStyle="shift-key" ... /> 113 * </pre> 114 */ 115 116 public class KeyboardBuilder<KP extends KeyboardParams> { 117 private static final String BUILDER_TAG = "Keyboard.Builder"; 118 private static final boolean DEBUG = false; 119 120 // Keyboard XML Tags 121 private static final String TAG_KEYBOARD = "Keyboard"; 122 private static final String TAG_ROW = "Row"; 123 private static final String TAG_KEY = "Key"; 124 private static final String TAG_SPACER = "Spacer"; 125 private static final String TAG_INCLUDE = "include"; 126 private static final String TAG_MERGE = "merge"; 127 private static final String TAG_SWITCH = "switch"; 128 private static final String TAG_CASE = "case"; 129 private static final String TAG_DEFAULT = "default"; 130 public static final String TAG_KEY_STYLE = "key-style"; 131 132 private static final int DEFAULT_KEYBOARD_COLUMNS = 10; 133 private static final int DEFAULT_KEYBOARD_ROWS = 4; 134 135 protected final KP mParams; 136 protected final Context mContext; 137 protected final Resources mResources; 138 139 private int mCurrentY = 0; 140 private KeyboardRow mCurrentRow = null; 141 private boolean mLeftEdge; 142 private boolean mTopEdge; 143 private Key mRightEdgeKey = null; 144 145 public KeyboardBuilder(final Context context, final KP params) { 146 mContext = context; 147 final Resources res = context.getResources(); 148 mResources = res; 149 150 mParams = params; 151 152 params.GRID_WIDTH = res.getInteger(R.integer.config_keyboard_grid_width); 153 params.GRID_HEIGHT = res.getInteger(R.integer.config_keyboard_grid_height); 154 } 155 156 public void setAutoGenerate(final KeysCache keysCache) { 157 mParams.mKeysCache = keysCache; 158 } 159 160 public KeyboardBuilder<KP> load(final int xmlId, final KeyboardId id) { 161 mParams.mId = id; 162 final XmlResourceParser parser = mResources.getXml(xmlId); 163 try { 164 parseKeyboard(parser); 165 } catch (XmlPullParserException e) { 166 Log.w(BUILDER_TAG, "keyboard XML parse error", e); 167 throw new IllegalArgumentException(e.getMessage(), e); 168 } catch (IOException e) { 169 Log.w(BUILDER_TAG, "keyboard XML parse error", e); 170 throw new RuntimeException(e.getMessage(), e); 171 } finally { 172 parser.close(); 173 } 174 return this; 175 } 176 177 @UsedForTesting 178 public void disableTouchPositionCorrectionDataForTest() { 179 mParams.mTouchPositionCorrection.setEnabled(false); 180 } 181 182 public void setProximityCharsCorrectionEnabled(final boolean enabled) { 183 mParams.mProximityCharsCorrectionEnabled = enabled; 184 } 185 186 public Keyboard build() { 187 return new Keyboard(mParams); 188 } 189 190 private int mIndent; 191 private static final String SPACES = " "; 192 193 private static String spaces(final int count) { 194 return (count < SPACES.length()) ? SPACES.substring(0, count) : SPACES; 195 } 196 197 private void startTag(final String format, final Object ... args) { 198 Log.d(BUILDER_TAG, String.format(spaces(++mIndent * 2) + format, args)); 199 } 200 201 private void endTag(final String format, final Object ... args) { 202 Log.d(BUILDER_TAG, String.format(spaces(mIndent-- * 2) + format, args)); 203 } 204 205 private void startEndTag(final String format, final Object ... args) { 206 Log.d(BUILDER_TAG, String.format(spaces(++mIndent * 2) + format, args)); 207 mIndent--; 208 } 209 210 private void parseKeyboard(final XmlPullParser parser) 211 throws XmlPullParserException, IOException { 212 if (DEBUG) startTag("<%s> %s", TAG_KEYBOARD, mParams.mId); 213 while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { 214 final int event = parser.next(); 215 if (event == XmlPullParser.START_TAG) { 216 final String tag = parser.getName(); 217 if (TAG_KEYBOARD.equals(tag)) { 218 parseKeyboardAttributes(parser); 219 startKeyboard(); 220 parseKeyboardContent(parser, false); 221 break; 222 } else { 223 throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD); 224 } 225 } 226 } 227 } 228 229 private void parseKeyboardAttributes(final XmlPullParser parser) { 230 final TypedArray keyboardAttr = mContext.obtainStyledAttributes( 231 Xml.asAttributeSet(parser), R.styleable.Keyboard, R.attr.keyboardStyle, 232 R.style.Keyboard); 233 final TypedArray keyAttr = mResources.obtainAttributes(Xml.asAttributeSet(parser), 234 R.styleable.Keyboard_Key); 235 try { 236 final KeyboardParams params = mParams; 237 final int height = params.mId.mHeight; 238 final int width = params.mId.mWidth; 239 params.mOccupiedHeight = height; 240 params.mOccupiedWidth = width; 241 params.mTopPadding = (int)keyboardAttr.getFraction( 242 R.styleable.Keyboard_keyboardTopPadding, height, height, 0); 243 params.mBottomPadding = (int)keyboardAttr.getFraction( 244 R.styleable.Keyboard_keyboardBottomPadding, height, height, 0); 245 params.mLeftPadding = (int)keyboardAttr.getFraction( 246 R.styleable.Keyboard_keyboardLeftPadding, width, width, 0); 247 params.mRightPadding = (int)keyboardAttr.getFraction( 248 R.styleable.Keyboard_keyboardRightPadding, width, width, 0); 249 250 final int baseWidth = 251 params.mOccupiedWidth - params.mLeftPadding - params.mRightPadding; 252 params.mBaseWidth = baseWidth; 253 params.mDefaultKeyWidth = (int)keyAttr.getFraction(R.styleable.Keyboard_Key_keyWidth, 254 baseWidth, baseWidth, baseWidth / DEFAULT_KEYBOARD_COLUMNS); 255 params.mHorizontalGap = (int)keyboardAttr.getFraction( 256 R.styleable.Keyboard_horizontalGap, baseWidth, baseWidth, 0); 257 // TODO: Fix keyboard geometry calculation clearer. Historically vertical gap between 258 // rows are determined based on the entire keyboard height including top and bottom 259 // paddings. 260 params.mVerticalGap = (int)keyboardAttr.getFraction( 261 R.styleable.Keyboard_verticalGap, height, height, 0); 262 final int baseHeight = params.mOccupiedHeight - params.mTopPadding 263 - params.mBottomPadding + params.mVerticalGap; 264 params.mBaseHeight = baseHeight; 265 params.mDefaultRowHeight = (int)ResourceUtils.getDimensionOrFraction(keyboardAttr, 266 R.styleable.Keyboard_rowHeight, baseHeight, baseHeight / DEFAULT_KEYBOARD_ROWS); 267 268 params.mKeyVisualAttributes = KeyVisualAttributes.newInstance(keyAttr); 269 270 params.mMoreKeysTemplate = keyboardAttr.getResourceId( 271 R.styleable.Keyboard_moreKeysTemplate, 0); 272 params.mMaxMoreKeysKeyboardColumn = keyAttr.getInt( 273 R.styleable.Keyboard_Key_maxMoreKeysColumn, 5); 274 275 params.mThemeId = keyboardAttr.getInt(R.styleable.Keyboard_themeId, 0); 276 params.mIconsSet.loadIcons(keyboardAttr); 277 final String language = params.mId.mLocale.getLanguage(); 278 params.mCodesSet.setLanguage(language); 279 params.mTextsSet.setLanguage(language); 280 final RunInLocale<Void> job = new RunInLocale<Void>() { 281 @Override 282 protected Void job(Resources res) { 283 params.mTextsSet.loadStringResources(mContext); 284 return null; 285 } 286 }; 287 // Null means the current system locale. 288 final Locale locale = SubtypeLocale.isNoLanguage(params.mId.mSubtype) 289 ? null : params.mId.mLocale; 290 job.runInLocale(mResources, locale); 291 292 final int resourceId = keyboardAttr.getResourceId( 293 R.styleable.Keyboard_touchPositionCorrectionData, 0); 294 if (resourceId != 0) { 295 final String[] data = mResources.getStringArray(resourceId); 296 params.mTouchPositionCorrection.load(data); 297 } 298 } finally { 299 keyAttr.recycle(); 300 keyboardAttr.recycle(); 301 } 302 } 303 304 private void parseKeyboardContent(final XmlPullParser parser, final boolean skip) 305 throws XmlPullParserException, IOException { 306 while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { 307 final int event = parser.next(); 308 if (event == XmlPullParser.START_TAG) { 309 final String tag = parser.getName(); 310 if (TAG_ROW.equals(tag)) { 311 final KeyboardRow row = parseRowAttributes(parser); 312 if (DEBUG) startTag("<%s>%s", TAG_ROW, skip ? " skipped" : ""); 313 if (!skip) { 314 startRow(row); 315 } 316 parseRowContent(parser, row, skip); 317 } else if (TAG_INCLUDE.equals(tag)) { 318 parseIncludeKeyboardContent(parser, skip); 319 } else if (TAG_SWITCH.equals(tag)) { 320 parseSwitchKeyboardContent(parser, skip); 321 } else if (TAG_KEY_STYLE.equals(tag)) { 322 parseKeyStyle(parser, skip); 323 } else { 324 throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_ROW); 325 } 326 } else if (event == XmlPullParser.END_TAG) { 327 final String tag = parser.getName(); 328 if (DEBUG) endTag("</%s>", tag); 329 if (TAG_KEYBOARD.equals(tag)) { 330 endKeyboard(); 331 break; 332 } else if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag) 333 || TAG_MERGE.equals(tag)) { 334 break; 335 } else { 336 throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_ROW); 337 } 338 } 339 } 340 } 341 342 private KeyboardRow parseRowAttributes(final XmlPullParser parser) 343 throws XmlPullParserException { 344 final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser), 345 R.styleable.Keyboard); 346 try { 347 if (a.hasValue(R.styleable.Keyboard_horizontalGap)) { 348 throw new XmlParseUtils.IllegalAttribute(parser, TAG_ROW, "horizontalGap"); 349 } 350 if (a.hasValue(R.styleable.Keyboard_verticalGap)) { 351 throw new XmlParseUtils.IllegalAttribute(parser, TAG_ROW, "verticalGap"); 352 } 353 return new KeyboardRow(mResources, mParams, parser, mCurrentY); 354 } finally { 355 a.recycle(); 356 } 357 } 358 359 private void parseRowContent(final XmlPullParser parser, final KeyboardRow row, 360 final boolean skip) throws XmlPullParserException, IOException { 361 while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { 362 final int event = parser.next(); 363 if (event == XmlPullParser.START_TAG) { 364 final String tag = parser.getName(); 365 if (TAG_KEY.equals(tag)) { 366 parseKey(parser, row, skip); 367 } else if (TAG_SPACER.equals(tag)) { 368 parseSpacer(parser, row, skip); 369 } else if (TAG_INCLUDE.equals(tag)) { 370 parseIncludeRowContent(parser, row, skip); 371 } else if (TAG_SWITCH.equals(tag)) { 372 parseSwitchRowContent(parser, row, skip); 373 } else if (TAG_KEY_STYLE.equals(tag)) { 374 parseKeyStyle(parser, skip); 375 } else { 376 throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_ROW); 377 } 378 } else if (event == XmlPullParser.END_TAG) { 379 final String tag = parser.getName(); 380 if (DEBUG) endTag("</%s>", tag); 381 if (TAG_ROW.equals(tag)) { 382 if (!skip) { 383 endRow(row); 384 } 385 break; 386 } else if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag) 387 || TAG_MERGE.equals(tag)) { 388 break; 389 } else { 390 throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_ROW); 391 } 392 } 393 } 394 } 395 396 private void parseKey(final XmlPullParser parser, final KeyboardRow row, final boolean skip) 397 throws XmlPullParserException, IOException { 398 if (skip) { 399 XmlParseUtils.checkEndTag(TAG_KEY, parser); 400 if (DEBUG) { 401 startEndTag("<%s /> skipped", TAG_KEY); 402 } 403 } else { 404 final Key key = new Key(mResources, mParams, row, parser); 405 if (DEBUG) { 406 startEndTag("<%s%s %s moreKeys=%s />", TAG_KEY, 407 (key.isEnabled() ? "" : " disabled"), key, 408 Arrays.toString(key.mMoreKeys)); 409 } 410 XmlParseUtils.checkEndTag(TAG_KEY, parser); 411 endKey(key); 412 } 413 } 414 415 private void parseSpacer(final XmlPullParser parser, final KeyboardRow row, final boolean skip) 416 throws XmlPullParserException, IOException { 417 if (skip) { 418 XmlParseUtils.checkEndTag(TAG_SPACER, parser); 419 if (DEBUG) startEndTag("<%s /> skipped", TAG_SPACER); 420 } else { 421 final Key.Spacer spacer = new Key.Spacer(mResources, mParams, row, parser); 422 if (DEBUG) startEndTag("<%s />", TAG_SPACER); 423 XmlParseUtils.checkEndTag(TAG_SPACER, parser); 424 endKey(spacer); 425 } 426 } 427 428 private void parseIncludeKeyboardContent(final XmlPullParser parser, final boolean skip) 429 throws XmlPullParserException, IOException { 430 parseIncludeInternal(parser, null, skip); 431 } 432 433 private void parseIncludeRowContent(final XmlPullParser parser, final KeyboardRow row, 434 final boolean skip) throws XmlPullParserException, IOException { 435 parseIncludeInternal(parser, row, skip); 436 } 437 438 private void parseIncludeInternal(final XmlPullParser parser, final KeyboardRow row, 439 final boolean skip) throws XmlPullParserException, IOException { 440 if (skip) { 441 XmlParseUtils.checkEndTag(TAG_INCLUDE, parser); 442 if (DEBUG) startEndTag("</%s> skipped", TAG_INCLUDE); 443 } else { 444 final AttributeSet attr = Xml.asAttributeSet(parser); 445 final TypedArray keyboardAttr = mResources.obtainAttributes(attr, 446 R.styleable.Keyboard_Include); 447 final TypedArray keyAttr = mResources.obtainAttributes(attr, 448 R.styleable.Keyboard_Key); 449 int keyboardLayout = 0; 450 float savedDefaultKeyWidth = 0; 451 int savedDefaultKeyLabelFlags = 0; 452 int savedDefaultBackgroundType = Key.BACKGROUND_TYPE_NORMAL; 453 try { 454 XmlParseUtils.checkAttributeExists(keyboardAttr, 455 R.styleable.Keyboard_Include_keyboardLayout, "keyboardLayout", 456 TAG_INCLUDE, parser); 457 keyboardLayout = keyboardAttr.getResourceId( 458 R.styleable.Keyboard_Include_keyboardLayout, 0); 459 if (row != null) { 460 if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyXPos)) { 461 // Override current x coordinate. 462 row.setXPos(row.getKeyX(keyAttr)); 463 } 464 // TODO: Remove this if-clause and do the same as backgroundType below. 465 savedDefaultKeyWidth = row.getDefaultKeyWidth(); 466 if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyWidth)) { 467 // Override default key width. 468 row.setDefaultKeyWidth(row.getKeyWidth(keyAttr)); 469 } 470 savedDefaultKeyLabelFlags = row.getDefaultKeyLabelFlags(); 471 // Bitwise-or default keyLabelFlag if exists. 472 row.setDefaultKeyLabelFlags(keyAttr.getInt( 473 R.styleable.Keyboard_Key_keyLabelFlags, 0) 474 | savedDefaultKeyLabelFlags); 475 savedDefaultBackgroundType = row.getDefaultBackgroundType(); 476 // Override default backgroundType if exists. 477 row.setDefaultBackgroundType(keyAttr.getInt( 478 R.styleable.Keyboard_Key_backgroundType, 479 savedDefaultBackgroundType)); 480 } 481 } finally { 482 keyboardAttr.recycle(); 483 keyAttr.recycle(); 484 } 485 486 XmlParseUtils.checkEndTag(TAG_INCLUDE, parser); 487 if (DEBUG) { 488 startEndTag("<%s keyboardLayout=%s />",TAG_INCLUDE, 489 mResources.getResourceEntryName(keyboardLayout)); 490 } 491 final XmlResourceParser parserForInclude = mResources.getXml(keyboardLayout); 492 try { 493 parseMerge(parserForInclude, row, skip); 494 } finally { 495 if (row != null) { 496 // Restore default keyWidth, keyLabelFlags, and backgroundType. 497 row.setDefaultKeyWidth(savedDefaultKeyWidth); 498 row.setDefaultKeyLabelFlags(savedDefaultKeyLabelFlags); 499 row.setDefaultBackgroundType(savedDefaultBackgroundType); 500 } 501 parserForInclude.close(); 502 } 503 } 504 } 505 506 private void parseMerge(final XmlPullParser parser, final KeyboardRow row, final boolean skip) 507 throws XmlPullParserException, IOException { 508 if (DEBUG) startTag("<%s>", TAG_MERGE); 509 while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { 510 final int event = parser.next(); 511 if (event == XmlPullParser.START_TAG) { 512 final String tag = parser.getName(); 513 if (TAG_MERGE.equals(tag)) { 514 if (row == null) { 515 parseKeyboardContent(parser, skip); 516 } else { 517 parseRowContent(parser, row, skip); 518 } 519 break; 520 } else { 521 throw new XmlParseUtils.ParseException( 522 "Included keyboard layout must have <merge> root element", parser); 523 } 524 } 525 } 526 } 527 528 private void parseSwitchKeyboardContent(final XmlPullParser parser, final boolean skip) 529 throws XmlPullParserException, IOException { 530 parseSwitchInternal(parser, null, skip); 531 } 532 533 private void parseSwitchRowContent(final XmlPullParser parser, final KeyboardRow row, 534 final boolean skip) throws XmlPullParserException, IOException { 535 parseSwitchInternal(parser, row, skip); 536 } 537 538 private void parseSwitchInternal(final XmlPullParser parser, final KeyboardRow row, 539 final boolean skip) throws XmlPullParserException, IOException { 540 if (DEBUG) startTag("<%s> %s", TAG_SWITCH, mParams.mId); 541 boolean selected = false; 542 while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { 543 final int event = parser.next(); 544 if (event == XmlPullParser.START_TAG) { 545 final String tag = parser.getName(); 546 if (TAG_CASE.equals(tag)) { 547 selected |= parseCase(parser, row, selected ? true : skip); 548 } else if (TAG_DEFAULT.equals(tag)) { 549 selected |= parseDefault(parser, row, selected ? true : skip); 550 } else { 551 throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_SWITCH); 552 } 553 } else if (event == XmlPullParser.END_TAG) { 554 final String tag = parser.getName(); 555 if (TAG_SWITCH.equals(tag)) { 556 if (DEBUG) endTag("</%s>", TAG_SWITCH); 557 break; 558 } else { 559 throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_SWITCH); 560 } 561 } 562 } 563 } 564 565 private boolean parseCase(final XmlPullParser parser, final KeyboardRow row, final boolean skip) 566 throws XmlPullParserException, IOException { 567 final boolean selected = parseCaseCondition(parser); 568 if (row == null) { 569 // Processing Rows. 570 parseKeyboardContent(parser, selected ? skip : true); 571 } else { 572 // Processing Keys. 573 parseRowContent(parser, row, selected ? skip : true); 574 } 575 return selected; 576 } 577 578 private boolean parseCaseCondition(final XmlPullParser parser) { 579 final KeyboardId id = mParams.mId; 580 if (id == null) { 581 return true; 582 } 583 final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser), 584 R.styleable.Keyboard_Case); 585 try { 586 final boolean keyboardLayoutSetElementMatched = matchTypedValue(a, 587 R.styleable.Keyboard_Case_keyboardLayoutSetElement, id.mElementId, 588 KeyboardId.elementIdToName(id.mElementId)); 589 final boolean modeMatched = matchTypedValue(a, 590 R.styleable.Keyboard_Case_mode, id.mMode, KeyboardId.modeName(id.mMode)); 591 final boolean navigateNextMatched = matchBoolean(a, 592 R.styleable.Keyboard_Case_navigateNext, id.navigateNext()); 593 final boolean navigatePreviousMatched = matchBoolean(a, 594 R.styleable.Keyboard_Case_navigatePrevious, id.navigatePrevious()); 595 final boolean passwordInputMatched = matchBoolean(a, 596 R.styleable.Keyboard_Case_passwordInput, id.passwordInput()); 597 final boolean clobberSettingsKeyMatched = matchBoolean(a, 598 R.styleable.Keyboard_Case_clobberSettingsKey, id.mClobberSettingsKey); 599 final boolean shortcutKeyEnabledMatched = matchBoolean(a, 600 R.styleable.Keyboard_Case_shortcutKeyEnabled, id.mShortcutKeyEnabled); 601 final boolean shortcutKeyOnSymbolsMatched = matchBoolean(a, 602 R.styleable.Keyboard_Case_shortcutKeyOnSymbols, id.mShortcutKeyOnSymbols); 603 final boolean hasShortcutKeyMatched = matchBoolean(a, 604 R.styleable.Keyboard_Case_hasShortcutKey, id.mHasShortcutKey); 605 final boolean languageSwitchKeyEnabledMatched = matchBoolean(a, 606 R.styleable.Keyboard_Case_languageSwitchKeyEnabled, 607 id.mLanguageSwitchKeyEnabled); 608 final boolean isMultiLineMatched = matchBoolean(a, 609 R.styleable.Keyboard_Case_isMultiLine, id.isMultiLine()); 610 final boolean imeActionMatched = matchInteger(a, 611 R.styleable.Keyboard_Case_imeAction, id.imeAction()); 612 final boolean localeCodeMatched = matchString(a, 613 R.styleable.Keyboard_Case_localeCode, id.mLocale.toString()); 614 final boolean languageCodeMatched = matchString(a, 615 R.styleable.Keyboard_Case_languageCode, id.mLocale.getLanguage()); 616 final boolean countryCodeMatched = matchString(a, 617 R.styleable.Keyboard_Case_countryCode, id.mLocale.getCountry()); 618 final boolean selected = keyboardLayoutSetElementMatched && modeMatched 619 && navigateNextMatched && navigatePreviousMatched && passwordInputMatched 620 && clobberSettingsKeyMatched && shortcutKeyEnabledMatched 621 && shortcutKeyOnSymbolsMatched && hasShortcutKeyMatched 622 && languageSwitchKeyEnabledMatched && isMultiLineMatched && imeActionMatched 623 && localeCodeMatched && languageCodeMatched && countryCodeMatched; 624 625 if (DEBUG) { 626 startTag("<%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s>%s", TAG_CASE, 627 textAttr(a.getString( 628 R.styleable.Keyboard_Case_keyboardLayoutSetElement), 629 "keyboardLayoutSetElement"), 630 textAttr(a.getString(R.styleable.Keyboard_Case_mode), "mode"), 631 textAttr(a.getString(R.styleable.Keyboard_Case_imeAction), 632 "imeAction"), 633 booleanAttr(a, R.styleable.Keyboard_Case_navigateNext, 634 "navigateNext"), 635 booleanAttr(a, R.styleable.Keyboard_Case_navigatePrevious, 636 "navigatePrevious"), 637 booleanAttr(a, R.styleable.Keyboard_Case_clobberSettingsKey, 638 "clobberSettingsKey"), 639 booleanAttr(a, R.styleable.Keyboard_Case_passwordInput, 640 "passwordInput"), 641 booleanAttr(a, R.styleable.Keyboard_Case_shortcutKeyEnabled, 642 "shortcutKeyEnabled"), 643 booleanAttr(a, R.styleable.Keyboard_Case_shortcutKeyOnSymbols, 644 "shortcutKeyOnSymbols"), 645 booleanAttr(a, R.styleable.Keyboard_Case_hasShortcutKey, 646 "hasShortcutKey"), 647 booleanAttr(a, R.styleable.Keyboard_Case_languageSwitchKeyEnabled, 648 "languageSwitchKeyEnabled"), 649 booleanAttr(a, R.styleable.Keyboard_Case_isMultiLine, 650 "isMultiLine"), 651 textAttr(a.getString(R.styleable.Keyboard_Case_localeCode), 652 "localeCode"), 653 textAttr(a.getString(R.styleable.Keyboard_Case_languageCode), 654 "languageCode"), 655 textAttr(a.getString(R.styleable.Keyboard_Case_countryCode), 656 "countryCode"), 657 selected ? "" : " skipped"); 658 } 659 660 return selected; 661 } finally { 662 a.recycle(); 663 } 664 } 665 666 private static boolean matchInteger(final TypedArray a, final int index, final int value) { 667 // If <case> does not have "index" attribute, that means this <case> is wild-card for 668 // the attribute. 669 return !a.hasValue(index) || a.getInt(index, 0) == value; 670 } 671 672 private static boolean matchBoolean(final TypedArray a, final int index, final boolean value) { 673 // If <case> does not have "index" attribute, that means this <case> is wild-card for 674 // the attribute. 675 return !a.hasValue(index) || a.getBoolean(index, false) == value; 676 } 677 678 private static boolean matchString(final TypedArray a, final int index, final String value) { 679 // If <case> does not have "index" attribute, that means this <case> is wild-card for 680 // the attribute. 681 return !a.hasValue(index) 682 || StringUtils.containsInArray(value, a.getString(index).split("\\|")); 683 } 684 685 private static boolean matchTypedValue(final TypedArray a, final int index, final int intValue, 686 final String strValue) { 687 // If <case> does not have "index" attribute, that means this <case> is wild-card for 688 // the attribute. 689 final TypedValue v = a.peekValue(index); 690 if (v == null) { 691 return true; 692 } 693 if (ResourceUtils.isIntegerValue(v)) { 694 return intValue == a.getInt(index, 0); 695 } else if (ResourceUtils.isStringValue(v)) { 696 return StringUtils.containsInArray(strValue, a.getString(index).split("\\|")); 697 } 698 return false; 699 } 700 701 private boolean parseDefault(final XmlPullParser parser, final KeyboardRow row, 702 final boolean skip) throws XmlPullParserException, IOException { 703 if (DEBUG) startTag("<%s>", TAG_DEFAULT); 704 if (row == null) { 705 parseKeyboardContent(parser, skip); 706 } else { 707 parseRowContent(parser, row, skip); 708 } 709 return true; 710 } 711 712 private void parseKeyStyle(final XmlPullParser parser, final boolean skip) 713 throws XmlPullParserException, IOException { 714 TypedArray keyStyleAttr = mResources.obtainAttributes(Xml.asAttributeSet(parser), 715 R.styleable.Keyboard_KeyStyle); 716 TypedArray keyAttrs = mResources.obtainAttributes(Xml.asAttributeSet(parser), 717 R.styleable.Keyboard_Key); 718 try { 719 if (!keyStyleAttr.hasValue(R.styleable.Keyboard_KeyStyle_styleName)) { 720 throw new XmlParseUtils.ParseException("<" + TAG_KEY_STYLE 721 + "/> needs styleName attribute", parser); 722 } 723 if (DEBUG) { 724 startEndTag("<%s styleName=%s />%s", TAG_KEY_STYLE, 725 keyStyleAttr.getString(R.styleable.Keyboard_KeyStyle_styleName), 726 skip ? " skipped" : ""); 727 } 728 if (!skip) { 729 mParams.mKeyStyles.parseKeyStyleAttributes(keyStyleAttr, keyAttrs, parser); 730 } 731 } finally { 732 keyStyleAttr.recycle(); 733 keyAttrs.recycle(); 734 } 735 XmlParseUtils.checkEndTag(TAG_KEY_STYLE, parser); 736 } 737 738 private void startKeyboard() { 739 mCurrentY += mParams.mTopPadding; 740 mTopEdge = true; 741 } 742 743 private void startRow(final KeyboardRow row) { 744 addEdgeSpace(mParams.mLeftPadding, row); 745 mCurrentRow = row; 746 mLeftEdge = true; 747 mRightEdgeKey = null; 748 } 749 750 private void endRow(final KeyboardRow row) { 751 if (mCurrentRow == null) { 752 throw new RuntimeException("orphan end row tag"); 753 } 754 if (mRightEdgeKey != null) { 755 mRightEdgeKey.markAsRightEdge(mParams); 756 mRightEdgeKey = null; 757 } 758 addEdgeSpace(mParams.mRightPadding, row); 759 mCurrentY += row.mRowHeight; 760 mCurrentRow = null; 761 mTopEdge = false; 762 } 763 764 private void endKey(final Key key) { 765 mParams.onAddKey(key); 766 if (mLeftEdge) { 767 key.markAsLeftEdge(mParams); 768 mLeftEdge = false; 769 } 770 if (mTopEdge) { 771 key.markAsTopEdge(mParams); 772 } 773 mRightEdgeKey = key; 774 } 775 776 private void endKeyboard() { 777 // nothing to do here. 778 } 779 780 private void addEdgeSpace(final float width, final KeyboardRow row) { 781 row.advanceXPos(width); 782 mLeftEdge = false; 783 mRightEdgeKey = null; 784 } 785 786 private static String textAttr(final String value, final String name) { 787 return value != null ? String.format(" %s=%s", name, value) : ""; 788 } 789 790 private static String booleanAttr(final TypedArray a, final int index, final String name) { 791 return a.hasValue(index) 792 ? String.format(" %s=%s", name, a.getBoolean(index, false)) : ""; 793 } 794 } 795