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