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