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