KeyboardLayoutSet.java revision aca2ef85e1af82ccadbd0cbdd691a680a03a824d
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.Constants.ImeOption.FORCE_ASCII; 20import static com.android.inputmethod.latin.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.LatinImeLogger; 40import com.android.inputmethod.latin.R; 41import com.android.inputmethod.latin.SubtypeSwitcher; 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 = LatinImeLogger.sDBG; 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 public ElementParams() {} 100 } 101 102 public static final class Params { 103 String mKeyboardLayoutSetName; 104 int mMode; 105 boolean mDisableTouchPositionCorrectionDataForTest; 106 // TODO: Use {@link InputAttributes} instead of these variables. 107 EditorInfo mEditorInfo; 108 boolean mIsPasswordField; 109 boolean mVoiceInputKeyEnabled; 110 boolean mNoSettingsKey; 111 boolean mLanguageSwitchKeyEnabled; 112 InputMethodSubtype mSubtype; 113 boolean mIsSpellChecker; 114 int mKeyboardWidth; 115 int mKeyboardHeight; 116 int mScriptId; 117 // Sparse array of KeyboardLayoutSet element parameters indexed by element's id. 118 final SparseArray<ElementParams> mKeyboardLayoutSetElementIdToParamsMap = 119 new SparseArray<>(); 120 } 121 122 public static void clearKeyboardCache() { 123 sKeyboardCache.clear(); 124 sKeysCache.clear(); 125 } 126 127 KeyboardLayoutSet(final Context context, final Params params) { 128 mContext = context; 129 mParams = params; 130 } 131 132 public Keyboard getKeyboard(final int baseKeyboardLayoutSetElementId) { 133 final int keyboardLayoutSetElementId; 134 switch (mParams.mMode) { 135 case KeyboardId.MODE_PHONE: 136 if (baseKeyboardLayoutSetElementId == KeyboardId.ELEMENT_SYMBOLS) { 137 keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE_SYMBOLS; 138 } else { 139 keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE; 140 } 141 break; 142 case KeyboardId.MODE_NUMBER: 143 case KeyboardId.MODE_DATE: 144 case KeyboardId.MODE_TIME: 145 case KeyboardId.MODE_DATETIME: 146 keyboardLayoutSetElementId = KeyboardId.ELEMENT_NUMBER; 147 break; 148 default: 149 keyboardLayoutSetElementId = baseKeyboardLayoutSetElementId; 150 break; 151 } 152 153 ElementParams elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get( 154 keyboardLayoutSetElementId); 155 if (elementParams == null) { 156 elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get( 157 KeyboardId.ELEMENT_ALPHABET); 158 } 159 // Note: The keyboard for each shift state, and mode are represented as an elementName 160 // attribute in a keyboard_layout_set XML file. Also each keyboard layout XML resource is 161 // specified as an elementKeyboard attribute in the file. 162 // The KeyboardId is an internal key for a Keyboard object. 163 final KeyboardId id = new KeyboardId(keyboardLayoutSetElementId, mParams); 164 try { 165 return getKeyboard(elementParams, id); 166 } catch (final RuntimeException e) { 167 Log.e(TAG, "Can't create keyboard: " + id, e); 168 throw new KeyboardLayoutSetException(e, id); 169 } 170 } 171 172 private Keyboard getKeyboard(final ElementParams elementParams, final KeyboardId id) { 173 final SoftReference<Keyboard> ref = sKeyboardCache.get(id); 174 final Keyboard cachedKeyboard = (ref == null) ? null : ref.get(); 175 if (cachedKeyboard != null) { 176 if (DEBUG_CACHE) { 177 Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": HIT id=" + id); 178 } 179 return cachedKeyboard; 180 } 181 182 final KeyboardBuilder<KeyboardParams> builder = 183 new KeyboardBuilder<>(mContext, new KeyboardParams()); 184 if (id.isAlphabetKeyboard()) { 185 builder.setAutoGenerate(sKeysCache); 186 } 187 final int keyboardXmlId = elementParams.mKeyboardXmlId; 188 builder.load(keyboardXmlId, id); 189 if (mParams.mDisableTouchPositionCorrectionDataForTest) { 190 builder.disableTouchPositionCorrectionDataForTest(); 191 } 192 builder.setProximityCharsCorrectionEnabled(elementParams.mProximityCharsCorrectionEnabled); 193 final Keyboard keyboard = builder.build(); 194 sKeyboardCache.put(id, new SoftReference<>(keyboard)); 195 if ((id.mElementId == KeyboardId.ELEMENT_ALPHABET 196 || id.mElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED) 197 && !mParams.mIsSpellChecker) { 198 // We only forcibly cache the primary, "ALPHABET", layouts. 199 for (int i = sForcibleKeyboardCache.length - 1; i >= 1; --i) { 200 sForcibleKeyboardCache[i] = sForcibleKeyboardCache[i - 1]; 201 } 202 sForcibleKeyboardCache[0] = keyboard; 203 if (DEBUG_CACHE) { 204 Log.d(TAG, "forcing caching of keyboard with id=" + id); 205 } 206 } 207 if (DEBUG_CACHE) { 208 Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": " 209 + ((ref == null) ? "LOAD" : "GCed") + " id=" + id); 210 } 211 return keyboard; 212 } 213 214 public static final class Builder { 215 private final Context mContext; 216 private final String mPackageName; 217 private final Resources mResources; 218 219 private final Params mParams = new Params(); 220 221 private static final EditorInfo EMPTY_EDITOR_INFO = new EditorInfo(); 222 223 public Builder(final Context context, final EditorInfo ei) { 224 mContext = context; 225 mPackageName = context.getPackageName(); 226 mResources = context.getResources(); 227 final Params params = mParams; 228 229 final EditorInfo editorInfo = (ei != null) ? ei : EMPTY_EDITOR_INFO; 230 params.mMode = getKeyboardMode(editorInfo); 231 // TODO: Consolidate those with {@link InputAttributes}. 232 params.mEditorInfo = editorInfo; 233 params.mIsPasswordField = InputTypeUtils.isPasswordInputType(editorInfo.inputType); 234 params.mNoSettingsKey = InputAttributes.inPrivateImeOptions( 235 mPackageName, NO_SETTINGS_KEY, editorInfo); 236 } 237 238 public Builder setKeyboardGeometry(final int keyboardWidth, final int keyboardHeight) { 239 mParams.mKeyboardWidth = keyboardWidth; 240 mParams.mKeyboardHeight = keyboardHeight; 241 return this; 242 } 243 244 public Builder setSubtype(final InputMethodSubtype subtype) { 245 final boolean asciiCapable = InputMethodSubtypeCompatUtils.isAsciiCapable(subtype); 246 // TODO: Consolidate with {@link InputAttributes}. 247 @SuppressWarnings("deprecation") 248 final boolean deprecatedForceAscii = InputAttributes.inPrivateImeOptions( 249 mPackageName, FORCE_ASCII, mParams.mEditorInfo); 250 final boolean forceAscii = EditorInfoCompatUtils.hasFlagForceAscii( 251 mParams.mEditorInfo.imeOptions) 252 || deprecatedForceAscii; 253 final InputMethodSubtype keyboardSubtype = (forceAscii && !asciiCapable) 254 ? SubtypeSwitcher.getInstance().getNoLanguageSubtype() 255 : subtype; 256 mParams.mSubtype = keyboardSubtype; 257 mParams.mKeyboardLayoutSetName = KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX 258 + SubtypeLocaleUtils.getKeyboardLayoutSetName(keyboardSubtype); 259 return this; 260 } 261 262 public Builder setIsSpellChecker(final boolean isSpellChecker) { 263 mParams.mIsSpellChecker = isSpellChecker; 264 return this; 265 } 266 267 public Builder setVoiceInputKeyEnabled(final boolean enabled) { 268 mParams.mVoiceInputKeyEnabled = enabled; 269 return this; 270 } 271 272 public Builder setLanguageSwitchKeyEnabled(final boolean enabled) { 273 mParams.mLanguageSwitchKeyEnabled = enabled; 274 return this; 275 } 276 277 public void disableTouchPositionCorrectionData() { 278 mParams.mDisableTouchPositionCorrectionDataForTest = true; 279 } 280 281 public void setScriptId(final int scriptId) { 282 mParams.mScriptId = scriptId; 283 } 284 285 public KeyboardLayoutSet build() { 286 if (mParams.mSubtype == null) 287 throw new RuntimeException("KeyboardLayoutSet subtype is not specified"); 288 final String packageName = mResources.getResourcePackageName( 289 R.xml.keyboard_layout_set_qwerty); 290 final String keyboardLayoutSetName = mParams.mKeyboardLayoutSetName; 291 final int xmlId = mResources.getIdentifier(keyboardLayoutSetName, "xml", packageName); 292 try { 293 parseKeyboardLayoutSet(mResources, xmlId); 294 } catch (final IOException e) { 295 throw new RuntimeException(e.getMessage() + " in " + keyboardLayoutSetName, e); 296 } catch (final XmlPullParserException e) { 297 throw new RuntimeException(e.getMessage() + " in " + keyboardLayoutSetName, e); 298 } 299 return new KeyboardLayoutSet(mContext, mParams); 300 } 301 302 private void parseKeyboardLayoutSet(final Resources res, final int resId) 303 throws XmlPullParserException, IOException { 304 final XmlResourceParser parser = res.getXml(resId); 305 try { 306 while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { 307 final int event = parser.next(); 308 if (event == XmlPullParser.START_TAG) { 309 final String tag = parser.getName(); 310 if (TAG_KEYBOARD_SET.equals(tag)) { 311 parseKeyboardLayoutSetContent(parser); 312 } else { 313 throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD_SET); 314 } 315 } 316 } 317 } finally { 318 parser.close(); 319 } 320 } 321 322 private void parseKeyboardLayoutSetContent(final XmlPullParser parser) 323 throws XmlPullParserException, IOException { 324 while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { 325 final int event = parser.next(); 326 if (event == XmlPullParser.START_TAG) { 327 final String tag = parser.getName(); 328 if (TAG_ELEMENT.equals(tag)) { 329 parseKeyboardLayoutSetElement(parser); 330 } else if (TAG_FEATURE.equals(tag)) { 331 parseKeyboardLayoutSetFeature(parser); 332 } else { 333 throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD_SET); 334 } 335 } else if (event == XmlPullParser.END_TAG) { 336 final String tag = parser.getName(); 337 if (TAG_KEYBOARD_SET.equals(tag)) { 338 break; 339 } else { 340 throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_KEYBOARD_SET); 341 } 342 } 343 } 344 } 345 346 private void parseKeyboardLayoutSetElement(final XmlPullParser parser) 347 throws XmlPullParserException, IOException { 348 final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser), 349 R.styleable.KeyboardLayoutSet_Element); 350 try { 351 XmlParseUtils.checkAttributeExists(a, 352 R.styleable.KeyboardLayoutSet_Element_elementName, "elementName", 353 TAG_ELEMENT, parser); 354 XmlParseUtils.checkAttributeExists(a, 355 R.styleable.KeyboardLayoutSet_Element_elementKeyboard, "elementKeyboard", 356 TAG_ELEMENT, parser); 357 XmlParseUtils.checkEndTag(TAG_ELEMENT, parser); 358 359 final ElementParams elementParams = new ElementParams(); 360 final int elementName = a.getInt( 361 R.styleable.KeyboardLayoutSet_Element_elementName, 0); 362 elementParams.mKeyboardXmlId = a.getResourceId( 363 R.styleable.KeyboardLayoutSet_Element_elementKeyboard, 0); 364 elementParams.mProximityCharsCorrectionEnabled = a.getBoolean( 365 R.styleable.KeyboardLayoutSet_Element_enableProximityCharsCorrection, 366 false); 367 mParams.mKeyboardLayoutSetElementIdToParamsMap.put(elementName, elementParams); 368 } finally { 369 a.recycle(); 370 } 371 } 372 373 private void parseKeyboardLayoutSetFeature(final XmlPullParser parser) 374 throws XmlPullParserException, IOException { 375 final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser), 376 R.styleable.KeyboardLayoutSet_Feature); 377 try { 378 final int scriptId = a.getInt( 379 R.styleable.KeyboardLayoutSet_Feature_supportedScript, 380 ScriptUtils.SCRIPT_LATIN); 381 XmlParseUtils.checkEndTag(TAG_FEATURE, parser); 382 setScriptId(scriptId); 383 } finally { 384 a.recycle(); 385 } 386 } 387 388 private static int getKeyboardMode(final EditorInfo editorInfo) { 389 final int inputType = editorInfo.inputType; 390 final int variation = inputType & InputType.TYPE_MASK_VARIATION; 391 392 switch (inputType & InputType.TYPE_MASK_CLASS) { 393 case InputType.TYPE_CLASS_NUMBER: 394 return KeyboardId.MODE_NUMBER; 395 case InputType.TYPE_CLASS_DATETIME: 396 switch (variation) { 397 case InputType.TYPE_DATETIME_VARIATION_DATE: 398 return KeyboardId.MODE_DATE; 399 case InputType.TYPE_DATETIME_VARIATION_TIME: 400 return KeyboardId.MODE_TIME; 401 default: // InputType.TYPE_DATETIME_VARIATION_NORMAL 402 return KeyboardId.MODE_DATETIME; 403 } 404 case InputType.TYPE_CLASS_PHONE: 405 return KeyboardId.MODE_PHONE; 406 case InputType.TYPE_CLASS_TEXT: 407 if (InputTypeUtils.isEmailVariation(variation)) { 408 return KeyboardId.MODE_EMAIL; 409 } else if (variation == InputType.TYPE_TEXT_VARIATION_URI) { 410 return KeyboardId.MODE_URL; 411 } else if (variation == InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE) { 412 return KeyboardId.MODE_IM; 413 } else if (variation == InputType.TYPE_TEXT_VARIATION_FILTER) { 414 return KeyboardId.MODE_TEXT; 415 } else { 416 return KeyboardId.MODE_TEXT; 417 } 418 default: 419 return KeyboardId.MODE_TEXT; 420 } 421 } 422 } 423} 424