KeyboardLayoutSet.java revision 2a7da0ab87db1166c62c171858b589da3d9c2ca7
1/* 2 * Copyright (C) 2011 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 17package com.android.inputmethod.keyboard; 18 19import static com.android.inputmethod.latin.common.Constants.ImeOption.FORCE_ASCII; 20import static com.android.inputmethod.latin.common.Constants.ImeOption.NO_SETTINGS_KEY; 21 22import android.content.Context; 23import android.content.res.Resources; 24import android.content.res.TypedArray; 25import android.content.res.XmlResourceParser; 26import android.text.InputType; 27import android.util.Log; 28import android.util.SparseArray; 29import android.util.Xml; 30import android.view.inputmethod.EditorInfo; 31import android.view.inputmethod.InputMethodSubtype; 32 33import com.android.inputmethod.compat.EditorInfoCompatUtils; 34import com.android.inputmethod.compat.InputMethodSubtypeCompatUtils; 35import com.android.inputmethod.keyboard.internal.KeyboardBuilder; 36import com.android.inputmethod.keyboard.internal.KeyboardParams; 37import com.android.inputmethod.keyboard.internal.KeysCache; 38import com.android.inputmethod.latin.InputAttributes; 39import com.android.inputmethod.latin.R; 40import com.android.inputmethod.latin.RichInputMethodSubtype; 41import com.android.inputmethod.latin.define.DebugFlags; 42import com.android.inputmethod.latin.utils.InputTypeUtils; 43import com.android.inputmethod.latin.utils.ScriptUtils; 44import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; 45import com.android.inputmethod.latin.utils.XmlParseUtils; 46 47import org.xmlpull.v1.XmlPullParser; 48import org.xmlpull.v1.XmlPullParserException; 49 50import java.io.IOException; 51import java.lang.ref.SoftReference; 52import java.util.HashMap; 53 54/** 55 * This class represents a set of keyboard layouts. Each of them represents a different keyboard 56 * specific to a keyboard state, such as alphabet, symbols, and so on. Layouts in the same 57 * {@link KeyboardLayoutSet} are related to each other. 58 * A {@link KeyboardLayoutSet} needs to be created for each 59 * {@link android.view.inputmethod.EditorInfo}. 60 */ 61public final class KeyboardLayoutSet { 62 private static final String TAG = KeyboardLayoutSet.class.getSimpleName(); 63 private static final boolean DEBUG_CACHE = DebugFlags.DEBUG_ENABLED; 64 65 private static final String TAG_KEYBOARD_SET = "KeyboardLayoutSet"; 66 private static final String TAG_ELEMENT = "Element"; 67 private static final String TAG_FEATURE = "Feature"; 68 69 private static final String KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX = "keyboard_layout_set_"; 70 71 private final Context mContext; 72 private final Params mParams; 73 74 // How many layouts we forcibly keep in cache. This only includes ALPHABET (default) and 75 // ALPHABET_AUTOMATIC_SHIFTED layouts - other layouts may stay in memory in the map of 76 // soft-references, but we forcibly cache this many alphabetic/auto-shifted layouts. 77 private static final int FORCIBLE_CACHE_SIZE = 4; 78 // By construction of soft references, anything that is also referenced somewhere else 79 // will stay in the cache. So we forcibly keep some references in an array to prevent 80 // them from disappearing from sKeyboardCache. 81 private static final Keyboard[] sForcibleKeyboardCache = new Keyboard[FORCIBLE_CACHE_SIZE]; 82 private static final HashMap<KeyboardId, SoftReference<Keyboard>> sKeyboardCache = 83 new HashMap<>(); 84 private static final KeysCache sKeysCache = new KeysCache(); 85 86 @SuppressWarnings("serial") 87 public static final class KeyboardLayoutSetException extends RuntimeException { 88 public final KeyboardId mKeyboardId; 89 90 public KeyboardLayoutSetException(final Throwable cause, final KeyboardId keyboardId) { 91 super(cause); 92 mKeyboardId = keyboardId; 93 } 94 } 95 96 private static final class ElementParams { 97 int mKeyboardXmlId; 98 boolean mProximityCharsCorrectionEnabled; 99 boolean mSupportsSplitLayout; 100 boolean mAllowRedundantMoreKeys; 101 public ElementParams() {} 102 } 103 104 public static final class Params { 105 String mKeyboardLayoutSetName; 106 int mMode; 107 boolean mDisableTouchPositionCorrectionDataForTest; 108 // TODO: Use {@link InputAttributes} instead of these variables. 109 EditorInfo mEditorInfo; 110 boolean mIsPasswordField; 111 boolean mVoiceInputKeyEnabled; 112 boolean mNoSettingsKey; 113 boolean mLanguageSwitchKeyEnabled; 114 RichInputMethodSubtype mSubtype; 115 boolean mIsSpellChecker; 116 int mKeyboardWidth; 117 int mKeyboardHeight; 118 int mScriptId = ScriptUtils.SCRIPT_LATIN; 119 // Indicates if the user has enabled the split-layout preference 120 // and the required ProductionFlags are enabled. 121 boolean mIsSplitLayoutEnabledByUser; 122 // Indicates if split layout is actually enabled, taking into account 123 // whether the user has enabled it, and the keyboard layout supports it. 124 boolean mIsSplitLayoutEnabled; 125 // Sparse array of KeyboardLayoutSet element parameters indexed by element's id. 126 final SparseArray<ElementParams> mKeyboardLayoutSetElementIdToParamsMap = 127 new SparseArray<>(); 128 } 129 130 public static void onSystemLocaleChanged() { 131 clearKeyboardCache(); 132 } 133 134 public static void onKeyboardThemeChanged() { 135 clearKeyboardCache(); 136 } 137 138 private static void clearKeyboardCache() { 139 sKeyboardCache.clear(); 140 sKeysCache.clear(); 141 } 142 143 KeyboardLayoutSet(final Context context, final Params params) { 144 mContext = context; 145 mParams = params; 146 } 147 148 public Keyboard getKeyboard(final int baseKeyboardLayoutSetElementId) { 149 final int keyboardLayoutSetElementId; 150 switch (mParams.mMode) { 151 case KeyboardId.MODE_PHONE: 152 if (baseKeyboardLayoutSetElementId == KeyboardId.ELEMENT_SYMBOLS) { 153 keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE_SYMBOLS; 154 } else { 155 keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE; 156 } 157 break; 158 case KeyboardId.MODE_NUMBER: 159 case KeyboardId.MODE_DATE: 160 case KeyboardId.MODE_TIME: 161 case KeyboardId.MODE_DATETIME: 162 keyboardLayoutSetElementId = KeyboardId.ELEMENT_NUMBER; 163 break; 164 default: 165 keyboardLayoutSetElementId = baseKeyboardLayoutSetElementId; 166 break; 167 } 168 169 ElementParams elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get( 170 keyboardLayoutSetElementId); 171 if (elementParams == null) { 172 elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get( 173 KeyboardId.ELEMENT_ALPHABET); 174 } 175 // Note: The keyboard for each shift state, and mode are represented as an elementName 176 // attribute in a keyboard_layout_set XML file. Also each keyboard layout XML resource is 177 // specified as an elementKeyboard attribute in the file. 178 // The KeyboardId is an internal key for a Keyboard object. 179 180 mParams.mIsSplitLayoutEnabled = mParams.mIsSplitLayoutEnabledByUser 181 && elementParams.mSupportsSplitLayout; 182 final KeyboardId id = new KeyboardId(keyboardLayoutSetElementId, mParams); 183 try { 184 return getKeyboard(elementParams, id); 185 } catch (final RuntimeException e) { 186 Log.e(TAG, "Can't create keyboard: " + id, e); 187 throw new KeyboardLayoutSetException(e, id); 188 } 189 } 190 191 private Keyboard getKeyboard(final ElementParams elementParams, final KeyboardId id) { 192 final SoftReference<Keyboard> ref = sKeyboardCache.get(id); 193 final Keyboard cachedKeyboard = (ref == null) ? null : ref.get(); 194 if (cachedKeyboard != null) { 195 if (DEBUG_CACHE) { 196 Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": HIT id=" + id); 197 } 198 return cachedKeyboard; 199 } 200 201 final KeyboardBuilder<KeyboardParams> builder = 202 new KeyboardBuilder<>(mContext, new KeyboardParams()); 203 if (id.isAlphabetKeyboard()) { 204 builder.setAutoGenerate(sKeysCache); 205 } 206 builder.setAllowRedundantMoreKes(elementParams.mAllowRedundantMoreKeys); 207 final int keyboardXmlId = elementParams.mKeyboardXmlId; 208 builder.load(keyboardXmlId, id); 209 if (mParams.mDisableTouchPositionCorrectionDataForTest) { 210 builder.disableTouchPositionCorrectionDataForTest(); 211 } 212 builder.setProximityCharsCorrectionEnabled(elementParams.mProximityCharsCorrectionEnabled); 213 final Keyboard keyboard = builder.build(); 214 sKeyboardCache.put(id, new SoftReference<>(keyboard)); 215 if ((id.mElementId == KeyboardId.ELEMENT_ALPHABET 216 || id.mElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED) 217 && !mParams.mIsSpellChecker) { 218 // We only forcibly cache the primary, "ALPHABET", layouts. 219 for (int i = sForcibleKeyboardCache.length - 1; i >= 1; --i) { 220 sForcibleKeyboardCache[i] = sForcibleKeyboardCache[i - 1]; 221 } 222 sForcibleKeyboardCache[0] = keyboard; 223 if (DEBUG_CACHE) { 224 Log.d(TAG, "forcing caching of keyboard with id=" + id); 225 } 226 } 227 if (DEBUG_CACHE) { 228 Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": " 229 + ((ref == null) ? "LOAD" : "GCed") + " id=" + id); 230 } 231 return keyboard; 232 } 233 234 public int getScriptId() { 235 return mParams.mScriptId; 236 } 237 238 public static final class Builder { 239 private final Context mContext; 240 private final String mPackageName; 241 private final Resources mResources; 242 243 private final Params mParams = new Params(); 244 245 private static final EditorInfo EMPTY_EDITOR_INFO = new EditorInfo(); 246 247 public Builder(final Context context, final EditorInfo ei) { 248 mContext = context; 249 mPackageName = context.getPackageName(); 250 mResources = context.getResources(); 251 final Params params = mParams; 252 253 final EditorInfo editorInfo = (ei != null) ? ei : EMPTY_EDITOR_INFO; 254 params.mMode = getKeyboardMode(editorInfo); 255 // TODO: Consolidate those with {@link InputAttributes}. 256 params.mEditorInfo = editorInfo; 257 params.mIsPasswordField = InputTypeUtils.isPasswordInputType(editorInfo.inputType); 258 params.mNoSettingsKey = InputAttributes.inPrivateImeOptions( 259 mPackageName, NO_SETTINGS_KEY, editorInfo); 260 } 261 262 public Builder setKeyboardGeometry(final int keyboardWidth, final int keyboardHeight) { 263 mParams.mKeyboardWidth = keyboardWidth; 264 mParams.mKeyboardHeight = keyboardHeight; 265 return this; 266 } 267 268 public Builder setSubtype(final RichInputMethodSubtype subtype) { 269 final boolean asciiCapable = InputMethodSubtypeCompatUtils.isAsciiCapable(subtype); 270 // TODO: Consolidate with {@link InputAttributes}. 271 @SuppressWarnings("deprecation") 272 final boolean deprecatedForceAscii = InputAttributes.inPrivateImeOptions( 273 mPackageName, FORCE_ASCII, mParams.mEditorInfo); 274 final boolean forceAscii = EditorInfoCompatUtils.hasFlagForceAscii( 275 mParams.mEditorInfo.imeOptions) 276 || deprecatedForceAscii; 277 final RichInputMethodSubtype keyboardSubtype = (forceAscii && !asciiCapable) 278 ? RichInputMethodSubtype.getNoLanguageSubtype() 279 : subtype; 280 mParams.mSubtype = keyboardSubtype; 281 mParams.mKeyboardLayoutSetName = KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX 282 + SubtypeLocaleUtils.getKeyboardLayoutSetName(keyboardSubtype); 283 return this; 284 } 285 286 public Builder setIsSpellChecker(final boolean isSpellChecker) { 287 mParams.mIsSpellChecker = isSpellChecker; 288 return this; 289 } 290 291 public Builder setVoiceInputKeyEnabled(final boolean enabled) { 292 mParams.mVoiceInputKeyEnabled = enabled; 293 return this; 294 } 295 296 public Builder setLanguageSwitchKeyEnabled(final boolean enabled) { 297 mParams.mLanguageSwitchKeyEnabled = enabled; 298 return this; 299 } 300 301 public Builder disableTouchPositionCorrectionData() { 302 mParams.mDisableTouchPositionCorrectionDataForTest = true; 303 return this; 304 } 305 306 public Builder setScriptId(final int scriptId) { 307 mParams.mScriptId = scriptId; 308 return this; 309 } 310 311 public Builder setSplitLayoutEnabledByUser(final boolean enabled) { 312 mParams.mIsSplitLayoutEnabledByUser = enabled; 313 return this; 314 } 315 316 private final static HashMap<InputMethodSubtype, Integer> sScriptIdsForSubtypes = 317 new HashMap<>(); 318 public static int getScriptId(final Resources resources, final InputMethodSubtype subtype) { 319 final Integer value = sScriptIdsForSubtypes.get(subtype); 320 if (null == value) { 321 final int scriptId = readScriptId(resources, subtype); 322 sScriptIdsForSubtypes.put(subtype, scriptId); 323 return scriptId; 324 } 325 return value; 326 } 327 328 // Super redux version of reading the script ID for some subtype from Xml. 329 private static int readScriptId(final Resources resources, 330 final InputMethodSubtype subtype) { 331 final String layoutSetName = KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX 332 + SubtypeLocaleUtils.getKeyboardLayoutSetName(subtype); 333 final int xmlId = getXmlId(resources, layoutSetName); 334 final XmlResourceParser parser = resources.getXml(xmlId); 335 try { 336 while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { 337 // Bovinate through the XML stupidly searching for TAG_FEATURE, and read 338 // the script Id from it. 339 parser.next(); 340 final String tag = parser.getName(); 341 if (TAG_FEATURE.equals(tag)) { 342 return readScriptIdFromTagFeature(resources, parser); 343 } 344 } 345 } catch (final IOException | XmlPullParserException e) { 346 throw new RuntimeException(e.getMessage() + " in " + layoutSetName, e); 347 } finally { 348 parser.close(); 349 } 350 // If the tag is not found, then the default script is Latin. 351 return ScriptUtils.SCRIPT_LATIN; 352 } 353 354 private static int readScriptIdFromTagFeature(final Resources resources, 355 final XmlPullParser parser) throws IOException, XmlPullParserException { 356 final TypedArray featureAttr = resources.obtainAttributes(Xml.asAttributeSet(parser), 357 R.styleable.KeyboardLayoutSet_Feature); 358 try { 359 final int scriptId = 360 featureAttr.getInt(R.styleable.KeyboardLayoutSet_Feature_supportedScript, 361 ScriptUtils.SCRIPT_UNKNOWN); 362 XmlParseUtils.checkEndTag(TAG_FEATURE, parser); 363 return scriptId; 364 } finally { 365 featureAttr.recycle(); 366 } 367 } 368 369 public KeyboardLayoutSet build() { 370 if (mParams.mSubtype == null) 371 throw new RuntimeException("KeyboardLayoutSet subtype is not specified"); 372 final int xmlId = getXmlId(mResources, mParams.mKeyboardLayoutSetName); 373 try { 374 parseKeyboardLayoutSet(mResources, xmlId); 375 } catch (final IOException | XmlPullParserException e) { 376 throw new RuntimeException(e.getMessage() + " in " + mParams.mKeyboardLayoutSetName, 377 e); 378 } 379 return new KeyboardLayoutSet(mContext, mParams); 380 } 381 382 private static int getXmlId(final Resources resources, final String keyboardLayoutSetName) { 383 final String packageName = resources.getResourcePackageName( 384 R.xml.keyboard_layout_set_qwerty); 385 return resources.getIdentifier(keyboardLayoutSetName, "xml", packageName); 386 } 387 388 private void parseKeyboardLayoutSet(final Resources res, final int resId) 389 throws XmlPullParserException, IOException { 390 final XmlResourceParser parser = res.getXml(resId); 391 try { 392 while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { 393 final int event = parser.next(); 394 if (event == XmlPullParser.START_TAG) { 395 final String tag = parser.getName(); 396 if (TAG_KEYBOARD_SET.equals(tag)) { 397 parseKeyboardLayoutSetContent(parser); 398 } else { 399 throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD_SET); 400 } 401 } 402 } 403 } finally { 404 parser.close(); 405 } 406 } 407 408 private void parseKeyboardLayoutSetContent(final XmlPullParser parser) 409 throws XmlPullParserException, IOException { 410 while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { 411 final int event = parser.next(); 412 if (event == XmlPullParser.START_TAG) { 413 final String tag = parser.getName(); 414 if (TAG_ELEMENT.equals(tag)) { 415 parseKeyboardLayoutSetElement(parser); 416 } else if (TAG_FEATURE.equals(tag)) { 417 parseKeyboardLayoutSetFeature(parser); 418 } else { 419 throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD_SET); 420 } 421 } else if (event == XmlPullParser.END_TAG) { 422 final String tag = parser.getName(); 423 if (TAG_KEYBOARD_SET.equals(tag)) { 424 break; 425 } 426 throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_KEYBOARD_SET); 427 } 428 } 429 } 430 431 private void parseKeyboardLayoutSetElement(final XmlPullParser parser) 432 throws XmlPullParserException, IOException { 433 final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser), 434 R.styleable.KeyboardLayoutSet_Element); 435 try { 436 XmlParseUtils.checkAttributeExists(a, 437 R.styleable.KeyboardLayoutSet_Element_elementName, "elementName", 438 TAG_ELEMENT, parser); 439 XmlParseUtils.checkAttributeExists(a, 440 R.styleable.KeyboardLayoutSet_Element_elementKeyboard, "elementKeyboard", 441 TAG_ELEMENT, parser); 442 XmlParseUtils.checkEndTag(TAG_ELEMENT, parser); 443 444 final ElementParams elementParams = new ElementParams(); 445 final int elementName = a.getInt( 446 R.styleable.KeyboardLayoutSet_Element_elementName, 0); 447 elementParams.mKeyboardXmlId = a.getResourceId( 448 R.styleable.KeyboardLayoutSet_Element_elementKeyboard, 0); 449 elementParams.mProximityCharsCorrectionEnabled = a.getBoolean( 450 R.styleable.KeyboardLayoutSet_Element_enableProximityCharsCorrection, 451 false); 452 elementParams.mSupportsSplitLayout = a.getBoolean( 453 R.styleable.KeyboardLayoutSet_Element_supportsSplitLayout, false); 454 elementParams.mAllowRedundantMoreKeys = a.getBoolean( 455 R.styleable.KeyboardLayoutSet_Element_allowRedundantMoreKeys, true); 456 mParams.mKeyboardLayoutSetElementIdToParamsMap.put(elementName, elementParams); 457 } finally { 458 a.recycle(); 459 } 460 } 461 462 private void parseKeyboardLayoutSetFeature(final XmlPullParser parser) 463 throws XmlPullParserException, IOException { 464 final int scriptId = readScriptIdFromTagFeature(mResources, parser); 465 setScriptId(scriptId); 466 } 467 468 private static int getKeyboardMode(final EditorInfo editorInfo) { 469 final int inputType = editorInfo.inputType; 470 final int variation = inputType & InputType.TYPE_MASK_VARIATION; 471 472 switch (inputType & InputType.TYPE_MASK_CLASS) { 473 case InputType.TYPE_CLASS_NUMBER: 474 return KeyboardId.MODE_NUMBER; 475 case InputType.TYPE_CLASS_DATETIME: 476 switch (variation) { 477 case InputType.TYPE_DATETIME_VARIATION_DATE: 478 return KeyboardId.MODE_DATE; 479 case InputType.TYPE_DATETIME_VARIATION_TIME: 480 return KeyboardId.MODE_TIME; 481 default: // InputType.TYPE_DATETIME_VARIATION_NORMAL 482 return KeyboardId.MODE_DATETIME; 483 } 484 case InputType.TYPE_CLASS_PHONE: 485 return KeyboardId.MODE_PHONE; 486 case InputType.TYPE_CLASS_TEXT: 487 if (InputTypeUtils.isEmailVariation(variation)) { 488 return KeyboardId.MODE_EMAIL; 489 } else if (variation == InputType.TYPE_TEXT_VARIATION_URI) { 490 return KeyboardId.MODE_URL; 491 } else if (variation == InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE) { 492 return KeyboardId.MODE_IM; 493 } else if (variation == InputType.TYPE_TEXT_VARIATION_FILTER) { 494 return KeyboardId.MODE_TEXT; 495 } else { 496 return KeyboardId.MODE_TEXT; 497 } 498 default: 499 return KeyboardId.MODE_TEXT; 500 } 501 } 502 } 503} 504