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