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