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