1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 17 package com.android.inputmethod.keyboard.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.DisplayMetrics; 24 import android.util.Log; 25 import android.util.TypedValue; 26 import android.util.Xml; 27 import android.view.InflateException; 28 29 import com.android.inputmethod.compat.EditorInfoCompatUtils; 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.LatinImeLogger; 34 import com.android.inputmethod.latin.R; 35 36 import org.xmlpull.v1.XmlPullParser; 37 import org.xmlpull.v1.XmlPullParserException; 38 39 import java.io.IOException; 40 import java.util.Arrays; 41 42 /** 43 * Keyboard Building helper. 44 * 45 * This class parses Keyboard XML file and eventually build a Keyboard. 46 * The Keyboard XML file looks like: 47 * <pre> 48 * >!-- xml/keyboard.xml --< 49 * >Keyboard keyboard_attributes*< 50 * >!-- Keyboard Content --< 51 * >Row row_attributes*< 52 * >!-- Row Content --< 53 * >Key key_attributes* /< 54 * >Spacer horizontalGap="0.2in" /< 55 * >include keyboardLayout="@xml/other_keys"< 56 * ... 57 * >/Row< 58 * >include keyboardLayout="@xml/other_rows"< 59 * ... 60 * >/Keyboard< 61 * </pre> 62 * The XML file which is included in other file must have >merge< as root element, such as: 63 * <pre> 64 * >!-- xml/other_keys.xml --< 65 * >merge< 66 * >Key key_attributes* /< 67 * ... 68 * >/merge< 69 * </pre> 70 * and 71 * <pre> 72 * >!-- xml/other_rows.xml --< 73 * >merge< 74 * >Row row_attributes*< 75 * >Key key_attributes* /< 76 * >/Row< 77 * ... 78 * >/merge< 79 * </pre> 80 * You can also use switch-case-default tags to select Rows and Keys. 81 * <pre> 82 * >switch< 83 * >case case_attribute*< 84 * >!-- Any valid tags at switch position --< 85 * >/case< 86 * ... 87 * >default< 88 * >!-- Any valid tags at switch position --< 89 * >/default< 90 * >/switch< 91 * </pre> 92 * You can declare Key style and specify styles within Key tags. 93 * <pre> 94 * >switch< 95 * >case mode="email"< 96 * >key-style styleName="f1-key" parentStyle="modifier-key" 97 * keyLabel=".com" 98 * /< 99 * >/case< 100 * >case mode="url"< 101 * >key-style styleName="f1-key" parentStyle="modifier-key" 102 * keyLabel="http://" 103 * /< 104 * >/case< 105 * >/switch< 106 * ... 107 * >Key keyStyle="shift-key" ... /< 108 * </pre> 109 */ 110 111 public class KeyboardBuilder<KP extends KeyboardParams> { 112 private static final String TAG = KeyboardBuilder.class.getSimpleName(); 113 private static final boolean DEBUG = false; 114 115 // Keyboard XML Tags 116 private static final String TAG_KEYBOARD = "Keyboard"; 117 private static final String TAG_ROW = "Row"; 118 private static final String TAG_KEY = "Key"; 119 private static final String TAG_SPACER = "Spacer"; 120 private static final String TAG_INCLUDE = "include"; 121 private static final String TAG_MERGE = "merge"; 122 private static final String TAG_SWITCH = "switch"; 123 private static final String TAG_CASE = "case"; 124 private static final String TAG_DEFAULT = "default"; 125 public static final String TAG_KEY_STYLE = "key-style"; 126 127 private static final int DEFAULT_KEYBOARD_COLUMNS = 10; 128 private static final int DEFAULT_KEYBOARD_ROWS = 4; 129 130 protected final KP mParams; 131 protected final Context mContext; 132 protected final Resources mResources; 133 private final DisplayMetrics mDisplayMetrics; 134 135 private int mCurrentY = 0; 136 private Row mCurrentRow = null; 137 private boolean mLeftEdge; 138 private boolean mTopEdge; 139 private Key mRightEdgeKey = null; 140 private final KeyStyles mKeyStyles = new KeyStyles(); 141 142 /** 143 * Container for keys in the keyboard. All keys in a row are at the same Y-coordinate. 144 * Some of the key size defaults can be overridden per row from what the {@link Keyboard} 145 * defines. 146 */ 147 public static class Row { 148 // keyWidth enum constants 149 private static final int KEYWIDTH_NOT_ENUM = 0; 150 private static final int KEYWIDTH_FILL_RIGHT = -1; 151 private static final int KEYWIDTH_FILL_BOTH = -2; 152 153 private final KeyboardParams mParams; 154 /** Default width of a key in this row. */ 155 public final float mDefaultKeyWidth; 156 /** Default height of a key in this row. */ 157 public final int mRowHeight; 158 159 private final int mCurrentY; 160 // Will be updated by {@link Key}'s constructor. 161 private float mCurrentX; 162 163 public Row(Resources res, KeyboardParams params, XmlPullParser parser, int y) { 164 mParams = params; 165 TypedArray keyboardAttr = res.obtainAttributes(Xml.asAttributeSet(parser), 166 R.styleable.Keyboard); 167 mRowHeight = (int)KeyboardBuilder.getDimensionOrFraction(keyboardAttr, 168 R.styleable.Keyboard_rowHeight, params.mBaseHeight, params.mDefaultRowHeight); 169 keyboardAttr.recycle(); 170 TypedArray keyAttr = res.obtainAttributes(Xml.asAttributeSet(parser), 171 R.styleable.Keyboard_Key); 172 mDefaultKeyWidth = KeyboardBuilder.getDimensionOrFraction(keyAttr, 173 R.styleable.Keyboard_Key_keyWidth, params.mBaseWidth, params.mDefaultKeyWidth); 174 keyAttr.recycle(); 175 176 mCurrentY = y; 177 mCurrentX = 0.0f; 178 } 179 180 public void setXPos(float keyXPos) { 181 mCurrentX = keyXPos; 182 } 183 184 public void advanceXPos(float width) { 185 mCurrentX += width; 186 } 187 188 public int getKeyY() { 189 return mCurrentY; 190 } 191 192 public float getKeyX(TypedArray keyAttr) { 193 final int widthType = KeyboardBuilder.getEnumValue(keyAttr, 194 R.styleable.Keyboard_Key_keyWidth, KEYWIDTH_NOT_ENUM); 195 if (widthType == KEYWIDTH_FILL_BOTH) { 196 // If keyWidth is fillBoth, the key width should start right after the nearest key 197 // on the left hand side. 198 return mCurrentX; 199 } 200 201 final int keyboardRightEdge = mParams.mOccupiedWidth - mParams.mHorizontalEdgesPadding; 202 if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyXPos)) { 203 final float keyXPos = KeyboardBuilder.getDimensionOrFraction(keyAttr, 204 R.styleable.Keyboard_Key_keyXPos, mParams.mBaseWidth, 0); 205 if (keyXPos < 0) { 206 // If keyXPos is negative, the actual x-coordinate will be 207 // keyboardWidth + keyXPos. 208 // keyXPos shouldn't be less than mCurrentX because drawable area for this key 209 // starts at mCurrentX. Or, this key will overlaps the adjacent key on its left 210 // hand side. 211 return Math.max(keyXPos + keyboardRightEdge, mCurrentX); 212 } else { 213 return keyXPos + mParams.mHorizontalEdgesPadding; 214 } 215 } 216 return mCurrentX; 217 } 218 219 public float getKeyWidth(TypedArray keyAttr, float keyXPos) { 220 final int widthType = KeyboardBuilder.getEnumValue(keyAttr, 221 R.styleable.Keyboard_Key_keyWidth, KEYWIDTH_NOT_ENUM); 222 switch (widthType) { 223 case KEYWIDTH_FILL_RIGHT: 224 case KEYWIDTH_FILL_BOTH: 225 final int keyboardRightEdge = 226 mParams.mOccupiedWidth - mParams.mHorizontalEdgesPadding; 227 // If keyWidth is fillRight, the actual key width will be determined to fill out the 228 // area up to the right edge of the keyboard. 229 // If keyWidth is fillBoth, the actual key width will be determined to fill out the 230 // area between the nearest key on the left hand side and the right edge of the 231 // keyboard. 232 return keyboardRightEdge - keyXPos; 233 default: // KEYWIDTH_NOT_ENUM 234 return KeyboardBuilder.getDimensionOrFraction(keyAttr, 235 R.styleable.Keyboard_Key_keyWidth, mParams.mBaseWidth, mDefaultKeyWidth); 236 } 237 } 238 } 239 240 public KeyboardBuilder(Context context, KP params) { 241 mContext = context; 242 final Resources res = context.getResources(); 243 mResources = res; 244 mDisplayMetrics = res.getDisplayMetrics(); 245 246 mParams = params; 247 248 setTouchPositionCorrectionData(context, params); 249 250 params.GRID_WIDTH = res.getInteger(R.integer.config_keyboard_grid_width); 251 params.GRID_HEIGHT = res.getInteger(R.integer.config_keyboard_grid_height); 252 } 253 254 private static void setTouchPositionCorrectionData(Context context, KeyboardParams params) { 255 final TypedArray a = context.obtainStyledAttributes( 256 null, R.styleable.Keyboard, R.attr.keyboardStyle, 0); 257 params.mThemeId = a.getInt(R.styleable.Keyboard_themeId, 0); 258 final int resourceId = a.getResourceId(R.styleable.Keyboard_touchPositionCorrectionData, 0); 259 a.recycle(); 260 if (resourceId == 0) { 261 if (LatinImeLogger.sDBG) 262 throw new RuntimeException("touchPositionCorrectionData is not defined"); 263 return; 264 } 265 266 final String[] data = context.getResources().getStringArray(resourceId); 267 params.mTouchPositionCorrection.load(data); 268 } 269 270 public KeyboardBuilder<KP> load(KeyboardId id) { 271 mParams.mId = id; 272 final XmlResourceParser parser = mResources.getXml(id.getXmlId()); 273 try { 274 parseKeyboard(parser); 275 } catch (XmlPullParserException e) { 276 Log.w(TAG, "keyboard XML parse error: " + e); 277 throw new IllegalArgumentException(e); 278 } catch (IOException e) { 279 Log.w(TAG, "keyboard XML parse error: " + e); 280 throw new RuntimeException(e); 281 } finally { 282 parser.close(); 283 } 284 return this; 285 } 286 287 public void setTouchPositionCorrectionEnabled(boolean enabled) { 288 mParams.mTouchPositionCorrection.setEnabled(enabled); 289 } 290 291 public Keyboard build() { 292 return new Keyboard(mParams); 293 } 294 295 private void parseKeyboard(XmlResourceParser parser) 296 throws XmlPullParserException, IOException { 297 if (DEBUG) Log.d(TAG, String.format("<%s> %s", TAG_KEYBOARD, mParams.mId)); 298 int event; 299 while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) { 300 if (event == XmlPullParser.START_TAG) { 301 final String tag = parser.getName(); 302 if (TAG_KEYBOARD.equals(tag)) { 303 parseKeyboardAttributes(parser); 304 startKeyboard(); 305 parseKeyboardContent(parser, false); 306 break; 307 } else { 308 throw new IllegalStartTag(parser, TAG_KEYBOARD); 309 } 310 } 311 } 312 } 313 314 public static String parseKeyboardLocale( 315 Context context, int resId) throws XmlPullParserException, IOException { 316 final Resources res = context.getResources(); 317 final XmlPullParser parser = res.getXml(resId); 318 if (parser == null) return ""; 319 int event; 320 while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) { 321 if (event == XmlPullParser.START_TAG) { 322 final String tag = parser.getName(); 323 if (TAG_KEYBOARD.equals(tag)) { 324 final TypedArray keyboardAttr = res.obtainAttributes(Xml.asAttributeSet(parser), 325 R.styleable.Keyboard); 326 final String locale = keyboardAttr.getString( 327 R.styleable.Keyboard_keyboardLocale); 328 keyboardAttr.recycle(); 329 return locale; 330 } else { 331 throw new IllegalStartTag(parser, TAG_KEYBOARD); 332 } 333 } 334 } 335 return ""; 336 } 337 338 private void parseKeyboardAttributes(XmlPullParser parser) { 339 final int displayWidth = mDisplayMetrics.widthPixels; 340 final TypedArray keyboardAttr = mContext.obtainStyledAttributes( 341 Xml.asAttributeSet(parser), R.styleable.Keyboard, R.attr.keyboardStyle, 342 R.style.Keyboard); 343 final TypedArray keyAttr = mResources.obtainAttributes(Xml.asAttributeSet(parser), 344 R.styleable.Keyboard_Key); 345 try { 346 final int displayHeight = mDisplayMetrics.heightPixels; 347 final int keyboardHeight = (int)keyboardAttr.getDimension( 348 R.styleable.Keyboard_keyboardHeight, displayHeight / 2); 349 final int maxKeyboardHeight = (int)getDimensionOrFraction(keyboardAttr, 350 R.styleable.Keyboard_maxKeyboardHeight, displayHeight, displayHeight / 2); 351 int minKeyboardHeight = (int)getDimensionOrFraction(keyboardAttr, 352 R.styleable.Keyboard_minKeyboardHeight, displayHeight, displayHeight / 2); 353 if (minKeyboardHeight < 0) { 354 // Specified fraction was negative, so it should be calculated against display 355 // width. 356 minKeyboardHeight = -(int)getDimensionOrFraction(keyboardAttr, 357 R.styleable.Keyboard_minKeyboardHeight, displayWidth, displayWidth / 2); 358 } 359 final KeyboardParams params = mParams; 360 // Keyboard height will not exceed maxKeyboardHeight and will not be less than 361 // minKeyboardHeight. 362 params.mOccupiedHeight = Math.max( 363 Math.min(keyboardHeight, maxKeyboardHeight), minKeyboardHeight); 364 params.mOccupiedWidth = params.mId.mWidth; 365 params.mTopPadding = (int)getDimensionOrFraction(keyboardAttr, 366 R.styleable.Keyboard_keyboardTopPadding, params.mOccupiedHeight, 0); 367 params.mBottomPadding = (int)getDimensionOrFraction(keyboardAttr, 368 R.styleable.Keyboard_keyboardBottomPadding, params.mOccupiedHeight, 0); 369 params.mHorizontalEdgesPadding = (int)getDimensionOrFraction(keyboardAttr, 370 R.styleable.Keyboard_keyboardHorizontalEdgesPadding, mParams.mOccupiedWidth, 0); 371 372 params.mBaseWidth = params.mOccupiedWidth - params.mHorizontalEdgesPadding * 2 373 - params.mHorizontalCenterPadding; 374 params.mDefaultKeyWidth = (int)getDimensionOrFraction(keyAttr, 375 R.styleable.Keyboard_Key_keyWidth, params.mBaseWidth, 376 params.mBaseWidth / DEFAULT_KEYBOARD_COLUMNS); 377 params.mHorizontalGap = (int)getDimensionOrFraction(keyboardAttr, 378 R.styleable.Keyboard_horizontalGap, params.mBaseWidth, 0); 379 params.mVerticalGap = (int)getDimensionOrFraction(keyboardAttr, 380 R.styleable.Keyboard_verticalGap, params.mOccupiedHeight, 0); 381 params.mBaseHeight = params.mOccupiedHeight - params.mTopPadding 382 - params.mBottomPadding + params.mVerticalGap; 383 params.mDefaultRowHeight = (int)getDimensionOrFraction(keyboardAttr, 384 R.styleable.Keyboard_rowHeight, params.mBaseHeight, 385 params.mBaseHeight / DEFAULT_KEYBOARD_ROWS); 386 387 params.mIsRtlKeyboard = keyboardAttr.getBoolean( 388 R.styleable.Keyboard_isRtlKeyboard, false); 389 params.mMoreKeysTemplate = keyboardAttr.getResourceId( 390 R.styleable.Keyboard_moreKeysTemplate, 0); 391 params.mMaxMiniKeyboardColumn = keyAttr.getInt( 392 R.styleable.Keyboard_Key_maxMoreKeysColumn, 5); 393 394 params.mIconsSet.loadIcons(keyboardAttr); 395 } finally { 396 keyAttr.recycle(); 397 keyboardAttr.recycle(); 398 } 399 } 400 401 private void parseKeyboardContent(XmlPullParser parser, boolean skip) 402 throws XmlPullParserException, IOException { 403 int event; 404 while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) { 405 if (event == XmlPullParser.START_TAG) { 406 final String tag = parser.getName(); 407 if (TAG_ROW.equals(tag)) { 408 Row row = parseRowAttributes(parser); 409 if (DEBUG) Log.d(TAG, String.format("<%s>", TAG_ROW)); 410 if (!skip) 411 startRow(row); 412 parseRowContent(parser, row, skip); 413 } else if (TAG_INCLUDE.equals(tag)) { 414 parseIncludeKeyboardContent(parser, skip); 415 } else if (TAG_SWITCH.equals(tag)) { 416 parseSwitchKeyboardContent(parser, skip); 417 } else if (TAG_KEY_STYLE.equals(tag)) { 418 parseKeyStyle(parser, skip); 419 } else { 420 throw new IllegalStartTag(parser, TAG_ROW); 421 } 422 } else if (event == XmlPullParser.END_TAG) { 423 final String tag = parser.getName(); 424 if (TAG_KEYBOARD.equals(tag)) { 425 endKeyboard(); 426 break; 427 } else if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag) 428 || TAG_MERGE.equals(tag)) { 429 if (DEBUG) Log.d(TAG, String.format("</%s>", tag)); 430 break; 431 } else if (TAG_KEY_STYLE.equals(tag)) { 432 continue; 433 } else { 434 throw new IllegalEndTag(parser, TAG_ROW); 435 } 436 } 437 } 438 } 439 440 private Row parseRowAttributes(XmlPullParser parser) { 441 final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser), 442 R.styleable.Keyboard); 443 try { 444 if (a.hasValue(R.styleable.Keyboard_horizontalGap)) 445 throw new IllegalAttribute(parser, "horizontalGap"); 446 if (a.hasValue(R.styleable.Keyboard_verticalGap)) 447 throw new IllegalAttribute(parser, "verticalGap"); 448 return new Row(mResources, mParams, parser, mCurrentY); 449 } finally { 450 a.recycle(); 451 } 452 } 453 454 private void parseRowContent(XmlPullParser parser, Row row, boolean skip) 455 throws XmlPullParserException, IOException { 456 int event; 457 while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) { 458 if (event == XmlPullParser.START_TAG) { 459 final String tag = parser.getName(); 460 if (TAG_KEY.equals(tag)) { 461 parseKey(parser, row, skip); 462 } else if (TAG_SPACER.equals(tag)) { 463 parseSpacer(parser, row, skip); 464 } else if (TAG_INCLUDE.equals(tag)) { 465 parseIncludeRowContent(parser, row, skip); 466 } else if (TAG_SWITCH.equals(tag)) { 467 parseSwitchRowContent(parser, row, skip); 468 } else if (TAG_KEY_STYLE.equals(tag)) { 469 parseKeyStyle(parser, skip); 470 } else { 471 throw new IllegalStartTag(parser, TAG_KEY); 472 } 473 } else if (event == XmlPullParser.END_TAG) { 474 final String tag = parser.getName(); 475 if (TAG_ROW.equals(tag)) { 476 if (DEBUG) Log.d(TAG, String.format("</%s>", TAG_ROW)); 477 if (!skip) 478 endRow(row); 479 break; 480 } else if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag) 481 || TAG_MERGE.equals(tag)) { 482 if (DEBUG) Log.d(TAG, String.format("</%s>", tag)); 483 break; 484 } else if (TAG_KEY_STYLE.equals(tag)) { 485 continue; 486 } else { 487 throw new IllegalEndTag(parser, TAG_KEY); 488 } 489 } 490 } 491 } 492 493 private void parseKey(XmlPullParser parser, Row row, boolean skip) 494 throws XmlPullParserException, IOException { 495 if (skip) { 496 checkEndTag(TAG_KEY, parser); 497 } else { 498 final Key key = new Key(mResources, mParams, row, parser, mKeyStyles); 499 if (DEBUG) Log.d(TAG, String.format("<%s%s keyLabel=%s code=%d moreKeys=%s />", 500 TAG_KEY, (key.isEnabled() ? "" : " disabled"), key.mLabel, key.mCode, 501 Arrays.toString(key.mMoreKeys))); 502 checkEndTag(TAG_KEY, parser); 503 endKey(key); 504 } 505 } 506 507 private void parseSpacer(XmlPullParser parser, Row row, boolean skip) 508 throws XmlPullParserException, IOException { 509 if (skip) { 510 checkEndTag(TAG_SPACER, parser); 511 } else { 512 final Key.Spacer spacer = new Key.Spacer(mResources, mParams, row, parser, mKeyStyles); 513 if (DEBUG) Log.d(TAG, String.format("<%s />", TAG_SPACER)); 514 checkEndTag(TAG_SPACER, parser); 515 endKey(spacer); 516 } 517 } 518 519 private void parseIncludeKeyboardContent(XmlPullParser parser, boolean skip) 520 throws XmlPullParserException, IOException { 521 parseIncludeInternal(parser, null, skip); 522 } 523 524 private void parseIncludeRowContent(XmlPullParser parser, Row row, boolean skip) 525 throws XmlPullParserException, IOException { 526 parseIncludeInternal(parser, row, skip); 527 } 528 529 private void parseIncludeInternal(XmlPullParser parser, Row row, boolean skip) 530 throws XmlPullParserException, IOException { 531 if (skip) { 532 checkEndTag(TAG_INCLUDE, parser); 533 } else { 534 final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser), 535 R.styleable.Keyboard_Include); 536 final int keyboardLayout = a.getResourceId( 537 R.styleable.Keyboard_Include_keyboardLayout, 0); 538 a.recycle(); 539 540 checkEndTag(TAG_INCLUDE, parser); 541 if (keyboardLayout == 0) 542 throw new ParseException("No keyboardLayout attribute in <include/>", parser); 543 if (DEBUG) Log.d(TAG, String.format("<%s keyboardLayout=%s />", 544 TAG_INCLUDE, mResources.getResourceEntryName(keyboardLayout))); 545 final XmlResourceParser parserForInclude = mResources.getXml(keyboardLayout); 546 try { 547 parseMerge(parserForInclude, row, skip); 548 } finally { 549 parserForInclude.close(); 550 } 551 } 552 } 553 554 private void parseMerge(XmlPullParser parser, Row row, boolean skip) 555 throws XmlPullParserException, IOException { 556 int event; 557 while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) { 558 if (event == XmlPullParser.START_TAG) { 559 final String tag = parser.getName(); 560 if (TAG_MERGE.equals(tag)) { 561 if (row == null) { 562 parseKeyboardContent(parser, skip); 563 } else { 564 parseRowContent(parser, row, skip); 565 } 566 break; 567 } else { 568 throw new ParseException( 569 "Included keyboard layout must have <merge> root element", parser); 570 } 571 } 572 } 573 } 574 575 private void parseSwitchKeyboardContent(XmlPullParser parser, boolean skip) 576 throws XmlPullParserException, IOException { 577 parseSwitchInternal(parser, null, skip); 578 } 579 580 private void parseSwitchRowContent(XmlPullParser parser, Row row, boolean skip) 581 throws XmlPullParserException, IOException { 582 parseSwitchInternal(parser, row, skip); 583 } 584 585 private void parseSwitchInternal(XmlPullParser parser, Row row, boolean skip) 586 throws XmlPullParserException, IOException { 587 if (DEBUG) Log.d(TAG, String.format("<%s> %s", TAG_SWITCH, mParams.mId)); 588 boolean selected = false; 589 int event; 590 while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) { 591 if (event == XmlPullParser.START_TAG) { 592 final String tag = parser.getName(); 593 if (TAG_CASE.equals(tag)) { 594 selected |= parseCase(parser, row, selected ? true : skip); 595 } else if (TAG_DEFAULT.equals(tag)) { 596 selected |= parseDefault(parser, row, selected ? true : skip); 597 } else { 598 throw new IllegalStartTag(parser, TAG_KEY); 599 } 600 } else if (event == XmlPullParser.END_TAG) { 601 final String tag = parser.getName(); 602 if (TAG_SWITCH.equals(tag)) { 603 if (DEBUG) Log.d(TAG, String.format("</%s>", TAG_SWITCH)); 604 break; 605 } else { 606 throw new IllegalEndTag(parser, TAG_KEY); 607 } 608 } 609 } 610 } 611 612 private boolean parseCase(XmlPullParser parser, Row row, boolean skip) 613 throws XmlPullParserException, IOException { 614 final boolean selected = parseCaseCondition(parser); 615 if (row == null) { 616 // Processing Rows. 617 parseKeyboardContent(parser, selected ? skip : true); 618 } else { 619 // Processing Keys. 620 parseRowContent(parser, row, selected ? skip : true); 621 } 622 return selected; 623 } 624 625 private boolean parseCaseCondition(XmlPullParser parser) { 626 final KeyboardId id = mParams.mId; 627 if (id == null) 628 return true; 629 630 final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser), 631 R.styleable.Keyboard_Case); 632 try { 633 final boolean modeMatched = matchTypedValue(a, 634 R.styleable.Keyboard_Case_mode, id.mMode, KeyboardId.modeName(id.mMode)); 635 final boolean navigateActionMatched = matchBoolean(a, 636 R.styleable.Keyboard_Case_navigateAction, id.mNavigateAction); 637 final boolean passwordInputMatched = matchBoolean(a, 638 R.styleable.Keyboard_Case_passwordInput, id.mPasswordInput); 639 final boolean hasSettingsKeyMatched = matchBoolean(a, 640 R.styleable.Keyboard_Case_hasSettingsKey, id.mHasSettingsKey); 641 final boolean f2KeyModeMatched = matchInteger(a, 642 R.styleable.Keyboard_Case_f2KeyMode, id.mF2KeyMode); 643 final boolean clobberSettingsKeyMatched = matchBoolean(a, 644 R.styleable.Keyboard_Case_clobberSettingsKey, id.mClobberSettingsKey); 645 final boolean shortcutKeyEnabledMatched = matchBoolean(a, 646 R.styleable.Keyboard_Case_shortcutKeyEnabled, id.mShortcutKeyEnabled); 647 final boolean hasShortcutKeyMatched = matchBoolean(a, 648 R.styleable.Keyboard_Case_hasShortcutKey, id.mHasShortcutKey); 649 // As noted at {@link KeyboardId} class, we are interested only in enum value masked by 650 // {@link android.view.inputmethod.EditorInfo#IME_MASK_ACTION} and 651 // {@link android.view.inputmethod.EditorInfo#IME_FLAG_NO_ENTER_ACTION}. So matching 652 // this attribute with id.mImeOptions as integer value is enough for our purpose. 653 final boolean imeActionMatched = matchInteger(a, 654 R.styleable.Keyboard_Case_imeAction, id.mImeAction); 655 final boolean localeCodeMatched = matchString(a, 656 R.styleable.Keyboard_Case_localeCode, id.mLocale.toString()); 657 final boolean languageCodeMatched = matchString(a, 658 R.styleable.Keyboard_Case_languageCode, id.mLocale.getLanguage()); 659 final boolean countryCodeMatched = matchString(a, 660 R.styleable.Keyboard_Case_countryCode, id.mLocale.getCountry()); 661 final boolean selected = modeMatched && navigateActionMatched && passwordInputMatched 662 && hasSettingsKeyMatched && f2KeyModeMatched && clobberSettingsKeyMatched 663 && shortcutKeyEnabledMatched && hasShortcutKeyMatched && imeActionMatched && 664 localeCodeMatched && languageCodeMatched && countryCodeMatched; 665 666 if (DEBUG) Log.d(TAG, String.format("<%s%s%s%s%s%s%s%s%s%s%s%s%s> %s", TAG_CASE, 667 textAttr(a.getString(R.styleable.Keyboard_Case_mode), "mode"), 668 booleanAttr(a, R.styleable.Keyboard_Case_navigateAction, "navigateAction"), 669 booleanAttr(a, R.styleable.Keyboard_Case_passwordInput, "passwordInput"), 670 booleanAttr(a, R.styleable.Keyboard_Case_hasSettingsKey, "hasSettingsKey"), 671 textAttr(KeyboardId.f2KeyModeName( 672 a.getInt(R.styleable.Keyboard_Case_f2KeyMode, -1)), "f2KeyMode"), 673 booleanAttr(a, R.styleable.Keyboard_Case_clobberSettingsKey, 674 "clobberSettingsKey"), 675 booleanAttr( 676 a, R.styleable.Keyboard_Case_shortcutKeyEnabled, "shortcutKeyEnabled"), 677 booleanAttr(a, R.styleable.Keyboard_Case_hasShortcutKey, "hasShortcutKey"), 678 textAttr(EditorInfoCompatUtils.imeOptionsName( 679 a.getInt(R.styleable.Keyboard_Case_imeAction, -1)), "imeAction"), 680 textAttr(a.getString(R.styleable.Keyboard_Case_localeCode), "localeCode"), 681 textAttr(a.getString(R.styleable.Keyboard_Case_languageCode), "languageCode"), 682 textAttr(a.getString(R.styleable.Keyboard_Case_countryCode), "countryCode"), 683 Boolean.toString(selected))); 684 685 return selected; 686 } finally { 687 a.recycle(); 688 } 689 } 690 691 private static boolean matchInteger(TypedArray a, int index, int value) { 692 // If <case> does not have "index" attribute, that means this <case> is wild-card for the 693 // attribute. 694 return !a.hasValue(index) || a.getInt(index, 0) == value; 695 } 696 697 private static boolean matchBoolean(TypedArray a, int index, boolean value) { 698 // If <case> does not have "index" attribute, that means this <case> is wild-card for the 699 // attribute. 700 return !a.hasValue(index) || a.getBoolean(index, false) == value; 701 } 702 703 private static boolean matchString(TypedArray a, int index, String value) { 704 // If <case> does not have "index" attribute, that means this <case> is wild-card for the 705 // attribute. 706 return !a.hasValue(index) || stringArrayContains(a.getString(index).split("\\|"), value); 707 } 708 709 private static boolean matchTypedValue(TypedArray a, int index, int intValue, String strValue) { 710 // If <case> does not have "index" attribute, that means this <case> is wild-card for the 711 // attribute. 712 final TypedValue v = a.peekValue(index); 713 if (v == null) 714 return true; 715 716 if (isIntegerValue(v)) { 717 return intValue == a.getInt(index, 0); 718 } else if (isStringValue(v)) { 719 return stringArrayContains(a.getString(index).split("\\|"), strValue); 720 } 721 return false; 722 } 723 724 private static boolean stringArrayContains(String[] array, String value) { 725 for (final String elem : array) { 726 if (elem.equals(value)) 727 return true; 728 } 729 return false; 730 } 731 732 private boolean parseDefault(XmlPullParser parser, Row row, boolean skip) 733 throws XmlPullParserException, IOException { 734 if (DEBUG) Log.d(TAG, String.format("<%s>", TAG_DEFAULT)); 735 if (row == null) { 736 parseKeyboardContent(parser, skip); 737 } else { 738 parseRowContent(parser, row, skip); 739 } 740 return true; 741 } 742 743 private void parseKeyStyle(XmlPullParser parser, boolean skip) { 744 TypedArray keyStyleAttr = mResources.obtainAttributes(Xml.asAttributeSet(parser), 745 R.styleable.Keyboard_KeyStyle); 746 TypedArray keyAttrs = mResources.obtainAttributes(Xml.asAttributeSet(parser), 747 R.styleable.Keyboard_Key); 748 try { 749 if (!keyStyleAttr.hasValue(R.styleable.Keyboard_KeyStyle_styleName)) 750 throw new ParseException("<" + TAG_KEY_STYLE 751 + "/> needs styleName attribute", parser); 752 if (!skip) 753 mKeyStyles.parseKeyStyleAttributes(keyStyleAttr, keyAttrs, parser); 754 } finally { 755 keyStyleAttr.recycle(); 756 keyAttrs.recycle(); 757 } 758 } 759 760 private static void checkEndTag(String tag, XmlPullParser parser) 761 throws XmlPullParserException, IOException { 762 if (parser.next() == XmlPullParser.END_TAG && tag.equals(parser.getName())) 763 return; 764 throw new NonEmptyTag(tag, parser); 765 } 766 767 private void startKeyboard() { 768 mCurrentY += mParams.mTopPadding; 769 mTopEdge = true; 770 } 771 772 private void startRow(Row row) { 773 addEdgeSpace(mParams.mHorizontalEdgesPadding, row); 774 mCurrentRow = row; 775 mLeftEdge = true; 776 mRightEdgeKey = null; 777 } 778 779 private void endRow(Row row) { 780 if (mCurrentRow == null) 781 throw new InflateException("orphant end row tag"); 782 if (mRightEdgeKey != null) { 783 mRightEdgeKey.markAsRightEdge(mParams); 784 mRightEdgeKey = null; 785 } 786 addEdgeSpace(mParams.mHorizontalEdgesPadding, row); 787 mCurrentY += row.mRowHeight; 788 mCurrentRow = null; 789 mTopEdge = false; 790 } 791 792 private void endKey(Key key) { 793 mParams.onAddKey(key); 794 if (mLeftEdge) { 795 key.markAsLeftEdge(mParams); 796 mLeftEdge = false; 797 } 798 if (mTopEdge) { 799 key.markAsTopEdge(mParams); 800 } 801 mRightEdgeKey = key; 802 } 803 804 private void endKeyboard() { 805 } 806 807 private void addEdgeSpace(float width, Row row) { 808 row.advanceXPos(width); 809 mLeftEdge = false; 810 mRightEdgeKey = null; 811 } 812 813 public static float getDimensionOrFraction(TypedArray a, int index, int base, float defValue) { 814 final TypedValue value = a.peekValue(index); 815 if (value == null) 816 return defValue; 817 if (isFractionValue(value)) { 818 return a.getFraction(index, base, base, defValue); 819 } else if (isDimensionValue(value)) { 820 return a.getDimension(index, defValue); 821 } 822 return defValue; 823 } 824 825 public static int getEnumValue(TypedArray a, int index, int defValue) { 826 final TypedValue value = a.peekValue(index); 827 if (value == null) 828 return defValue; 829 if (isIntegerValue(value)) { 830 return a.getInt(index, defValue); 831 } 832 return defValue; 833 } 834 835 private static boolean isFractionValue(TypedValue v) { 836 return v.type == TypedValue.TYPE_FRACTION; 837 } 838 839 private static boolean isDimensionValue(TypedValue v) { 840 return v.type == TypedValue.TYPE_DIMENSION; 841 } 842 843 private static boolean isIntegerValue(TypedValue v) { 844 return v.type >= TypedValue.TYPE_FIRST_INT && v.type <= TypedValue.TYPE_LAST_INT; 845 } 846 847 private static boolean isStringValue(TypedValue v) { 848 return v.type == TypedValue.TYPE_STRING; 849 } 850 851 @SuppressWarnings("serial") 852 public static class ParseException extends InflateException { 853 public ParseException(String msg, XmlPullParser parser) { 854 super(msg + " at line " + parser.getLineNumber()); 855 } 856 } 857 858 @SuppressWarnings("serial") 859 private static class IllegalStartTag extends ParseException { 860 public IllegalStartTag(XmlPullParser parser, String parent) { 861 super("Illegal start tag " + parser.getName() + " in " + parent, parser); 862 } 863 } 864 865 @SuppressWarnings("serial") 866 private static class IllegalEndTag extends ParseException { 867 public IllegalEndTag(XmlPullParser parser, String parent) { 868 super("Illegal end tag " + parser.getName() + " in " + parent, parser); 869 } 870 } 871 872 @SuppressWarnings("serial") 873 private static class IllegalAttribute extends ParseException { 874 public IllegalAttribute(XmlPullParser parser, String attribute) { 875 super("Tag " + parser.getName() + " has illegal attribute " + attribute, parser); 876 } 877 } 878 879 @SuppressWarnings("serial") 880 private static class NonEmptyTag extends ParseException { 881 public NonEmptyTag(String tag, XmlPullParser parser) { 882 super(tag + " must be empty tag", parser); 883 } 884 } 885 886 private static String textAttr(String value, String name) { 887 return value != null ? String.format(" %s=%s", name, value) : ""; 888 } 889 890 private static String booleanAttr(TypedArray a, int index, String name) { 891 return a.hasValue(index) ? String.format(" %s=%s", name, a.getBoolean(index, false)) : ""; 892 } 893 } 894