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 17 package com.android.inputmethod.keyboard; 18 19 import static com.android.inputmethod.latin.common.Constants.ImeOption.FORCE_ASCII; 20 import static com.android.inputmethod.latin.common.Constants.ImeOption.NO_SETTINGS_KEY; 21 22 import android.content.Context; 23 import android.content.res.Resources; 24 import android.content.res.TypedArray; 25 import android.content.res.XmlResourceParser; 26 import android.text.InputType; 27 import android.util.Log; 28 import android.util.SparseArray; 29 import android.util.Xml; 30 import android.view.inputmethod.EditorInfo; 31 import android.view.inputmethod.InputMethodSubtype; 32 33 import com.android.inputmethod.compat.EditorInfoCompatUtils; 34 import com.android.inputmethod.compat.InputMethodSubtypeCompatUtils; 35 import com.android.inputmethod.keyboard.internal.KeyboardBuilder; 36 import com.android.inputmethod.keyboard.internal.KeyboardParams; 37 import com.android.inputmethod.keyboard.internal.UniqueKeysCache; 38 import com.android.inputmethod.latin.InputAttributes; 39 import com.android.inputmethod.latin.R; 40 import com.android.inputmethod.latin.RichInputMethodSubtype; 41 import com.android.inputmethod.latin.define.DebugFlags; 42 import com.android.inputmethod.latin.utils.InputTypeUtils; 43 import com.android.inputmethod.latin.utils.ScriptUtils; 44 import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; 45 import com.android.inputmethod.latin.utils.XmlParseUtils; 46 47 import org.xmlpull.v1.XmlPullParser; 48 import org.xmlpull.v1.XmlPullParserException; 49 50 import java.io.IOException; 51 import java.lang.ref.SoftReference; 52 import java.util.HashMap; 53 54 import javax.annotation.Nonnull; 55 import javax.annotation.Nullable; 56 57 /** 58 * This class represents a set of keyboard layouts. Each of them represents a different keyboard 59 * specific to a keyboard state, such as alphabet, symbols, and so on. Layouts in the same 60 * {@link KeyboardLayoutSet} are related to each other. 61 * A {@link KeyboardLayoutSet} needs to be created for each 62 * {@link android.view.inputmethod.EditorInfo}. 63 */ 64 public final class KeyboardLayoutSet { 65 private static final String TAG = KeyboardLayoutSet.class.getSimpleName(); 66 private static final boolean DEBUG_CACHE = false; 67 68 private static final String TAG_KEYBOARD_SET = "KeyboardLayoutSet"; 69 private static final String TAG_ELEMENT = "Element"; 70 private static final String TAG_FEATURE = "Feature"; 71 72 private static final String KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX = "keyboard_layout_set_"; 73 74 private final Context mContext; 75 @Nonnull 76 private final Params mParams; 77 78 // How many layouts we forcibly keep in cache. This only includes ALPHABET (default) and 79 // ALPHABET_AUTOMATIC_SHIFTED layouts - other layouts may stay in memory in the map of 80 // soft-references, but we forcibly cache this many alphabetic/auto-shifted layouts. 81 private static final int FORCIBLE_CACHE_SIZE = 4; 82 // By construction of soft references, anything that is also referenced somewhere else 83 // will stay in the cache. So we forcibly keep some references in an array to prevent 84 // them from disappearing from sKeyboardCache. 85 private static final Keyboard[] sForcibleKeyboardCache = new Keyboard[FORCIBLE_CACHE_SIZE]; 86 private static final HashMap<KeyboardId, SoftReference<Keyboard>> sKeyboardCache = 87 new HashMap<>(); 88 @Nonnull 89 private static final UniqueKeysCache sUniqueKeysCache = UniqueKeysCache.newInstance(); 90 private final static HashMap<InputMethodSubtype, Integer> sScriptIdsForSubtypes = 91 new HashMap<>(); 92 93 @SuppressWarnings("serial") 94 public static final class KeyboardLayoutSetException extends RuntimeException { 95 public final KeyboardId mKeyboardId; 96 KeyboardLayoutSetException(final Throwable cause, final KeyboardId keyboardId)97 public KeyboardLayoutSetException(final Throwable cause, final KeyboardId keyboardId) { 98 super(cause); 99 mKeyboardId = keyboardId; 100 } 101 } 102 103 private static final class ElementParams { 104 int mKeyboardXmlId; 105 boolean mProximityCharsCorrectionEnabled; 106 boolean mSupportsSplitLayout; 107 boolean mAllowRedundantMoreKeys; ElementParams()108 public ElementParams() {} 109 } 110 111 public static final class Params { 112 String mKeyboardLayoutSetName; 113 int mMode; 114 boolean mDisableTouchPositionCorrectionDataForTest; 115 // TODO: Use {@link InputAttributes} instead of these variables. 116 EditorInfo mEditorInfo; 117 boolean mIsPasswordField; 118 boolean mVoiceInputKeyEnabled; 119 boolean mNoSettingsKey; 120 boolean mLanguageSwitchKeyEnabled; 121 RichInputMethodSubtype mSubtype; 122 boolean mIsSpellChecker; 123 int mKeyboardWidth; 124 int mKeyboardHeight; 125 int mScriptId = ScriptUtils.SCRIPT_LATIN; 126 // Indicates if the user has enabled the split-layout preference 127 // and the required ProductionFlags are enabled. 128 boolean mIsSplitLayoutEnabledByUser; 129 // Indicates if split layout is actually enabled, taking into account 130 // whether the user has enabled it, and the keyboard layout supports it. 131 boolean mIsSplitLayoutEnabled; 132 // Sparse array of KeyboardLayoutSet element parameters indexed by element's id. 133 final SparseArray<ElementParams> mKeyboardLayoutSetElementIdToParamsMap = 134 new SparseArray<>(); 135 } 136 onSystemLocaleChanged()137 public static void onSystemLocaleChanged() { 138 clearKeyboardCache(); 139 } 140 onKeyboardThemeChanged()141 public static void onKeyboardThemeChanged() { 142 clearKeyboardCache(); 143 } 144 clearKeyboardCache()145 private static void clearKeyboardCache() { 146 sKeyboardCache.clear(); 147 sUniqueKeysCache.clear(); 148 } 149 getScriptId(final Resources resources, @Nonnull final InputMethodSubtype subtype)150 public static int getScriptId(final Resources resources, 151 @Nonnull final InputMethodSubtype subtype) { 152 final Integer value = sScriptIdsForSubtypes.get(subtype); 153 if (null == value) { 154 final int scriptId = Builder.readScriptId(resources, subtype); 155 sScriptIdsForSubtypes.put(subtype, scriptId); 156 return scriptId; 157 } 158 return value; 159 } 160 KeyboardLayoutSet(final Context context, @Nonnull final Params params)161 KeyboardLayoutSet(final Context context, @Nonnull final Params params) { 162 mContext = context; 163 mParams = params; 164 } 165 166 @Nonnull getKeyboard(final int baseKeyboardLayoutSetElementId)167 public Keyboard getKeyboard(final int baseKeyboardLayoutSetElementId) { 168 final int keyboardLayoutSetElementId; 169 switch (mParams.mMode) { 170 case KeyboardId.MODE_PHONE: 171 if (baseKeyboardLayoutSetElementId == KeyboardId.ELEMENT_SYMBOLS) { 172 keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE_SYMBOLS; 173 } else { 174 keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE; 175 } 176 break; 177 case KeyboardId.MODE_NUMBER: 178 case KeyboardId.MODE_DATE: 179 case KeyboardId.MODE_TIME: 180 case KeyboardId.MODE_DATETIME: 181 keyboardLayoutSetElementId = KeyboardId.ELEMENT_NUMBER; 182 break; 183 default: 184 keyboardLayoutSetElementId = baseKeyboardLayoutSetElementId; 185 break; 186 } 187 188 ElementParams elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get( 189 keyboardLayoutSetElementId); 190 if (elementParams == null) { 191 elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get( 192 KeyboardId.ELEMENT_ALPHABET); 193 } 194 // Note: The keyboard for each shift state, and mode are represented as an elementName 195 // attribute in a keyboard_layout_set XML file. Also each keyboard layout XML resource is 196 // specified as an elementKeyboard attribute in the file. 197 // The KeyboardId is an internal key for a Keyboard object. 198 199 mParams.mIsSplitLayoutEnabled = mParams.mIsSplitLayoutEnabledByUser 200 && elementParams.mSupportsSplitLayout; 201 final KeyboardId id = new KeyboardId(keyboardLayoutSetElementId, mParams); 202 try { 203 return getKeyboard(elementParams, id); 204 } catch (final RuntimeException e) { 205 Log.e(TAG, "Can't create keyboard: " + id, e); 206 throw new KeyboardLayoutSetException(e, id); 207 } 208 } 209 210 @Nonnull getKeyboard(final ElementParams elementParams, final KeyboardId id)211 private Keyboard getKeyboard(final ElementParams elementParams, final KeyboardId id) { 212 final SoftReference<Keyboard> ref = sKeyboardCache.get(id); 213 final Keyboard cachedKeyboard = (ref == null) ? null : ref.get(); 214 if (cachedKeyboard != null) { 215 if (DEBUG_CACHE) { 216 Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": HIT id=" + id); 217 } 218 return cachedKeyboard; 219 } 220 221 final KeyboardBuilder<KeyboardParams> builder = 222 new KeyboardBuilder<>(mContext, new KeyboardParams(sUniqueKeysCache)); 223 sUniqueKeysCache.setEnabled(id.isAlphabetKeyboard()); 224 builder.setAllowRedundantMoreKes(elementParams.mAllowRedundantMoreKeys); 225 final int keyboardXmlId = elementParams.mKeyboardXmlId; 226 builder.load(keyboardXmlId, id); 227 if (mParams.mDisableTouchPositionCorrectionDataForTest) { 228 builder.disableTouchPositionCorrectionDataForTest(); 229 } 230 builder.setProximityCharsCorrectionEnabled(elementParams.mProximityCharsCorrectionEnabled); 231 final Keyboard keyboard = builder.build(); 232 sKeyboardCache.put(id, new SoftReference<>(keyboard)); 233 if ((id.mElementId == KeyboardId.ELEMENT_ALPHABET 234 || id.mElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED) 235 && !mParams.mIsSpellChecker) { 236 // We only forcibly cache the primary, "ALPHABET", layouts. 237 for (int i = sForcibleKeyboardCache.length - 1; i >= 1; --i) { 238 sForcibleKeyboardCache[i] = sForcibleKeyboardCache[i - 1]; 239 } 240 sForcibleKeyboardCache[0] = keyboard; 241 if (DEBUG_CACHE) { 242 Log.d(TAG, "forcing caching of keyboard with id=" + id); 243 } 244 } 245 if (DEBUG_CACHE) { 246 Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": " 247 + ((ref == null) ? "LOAD" : "GCed") + " id=" + id); 248 } 249 return keyboard; 250 } 251 getScriptId()252 public int getScriptId() { 253 return mParams.mScriptId; 254 } 255 256 public static final class Builder { 257 private final Context mContext; 258 private final String mPackageName; 259 private final Resources mResources; 260 261 private final Params mParams = new Params(); 262 263 private static final EditorInfo EMPTY_EDITOR_INFO = new EditorInfo(); 264 Builder(final Context context, @Nullable final EditorInfo ei)265 public Builder(final Context context, @Nullable final EditorInfo ei) { 266 mContext = context; 267 mPackageName = context.getPackageName(); 268 mResources = context.getResources(); 269 final Params params = mParams; 270 271 final EditorInfo editorInfo = (ei != null) ? ei : EMPTY_EDITOR_INFO; 272 params.mMode = getKeyboardMode(editorInfo); 273 // TODO: Consolidate those with {@link InputAttributes}. 274 params.mEditorInfo = editorInfo; 275 params.mIsPasswordField = InputTypeUtils.isPasswordInputType(editorInfo.inputType); 276 params.mNoSettingsKey = InputAttributes.inPrivateImeOptions( 277 mPackageName, NO_SETTINGS_KEY, editorInfo); 278 } 279 setKeyboardGeometry(final int keyboardWidth, final int keyboardHeight)280 public Builder setKeyboardGeometry(final int keyboardWidth, final int keyboardHeight) { 281 mParams.mKeyboardWidth = keyboardWidth; 282 mParams.mKeyboardHeight = keyboardHeight; 283 return this; 284 } 285 setSubtype(@onnull final RichInputMethodSubtype subtype)286 public Builder setSubtype(@Nonnull final RichInputMethodSubtype subtype) { 287 final boolean asciiCapable = InputMethodSubtypeCompatUtils.isAsciiCapable(subtype); 288 // TODO: Consolidate with {@link InputAttributes}. 289 @SuppressWarnings("deprecation") 290 final boolean deprecatedForceAscii = InputAttributes.inPrivateImeOptions( 291 mPackageName, FORCE_ASCII, mParams.mEditorInfo); 292 final boolean forceAscii = EditorInfoCompatUtils.hasFlagForceAscii( 293 mParams.mEditorInfo.imeOptions) 294 || deprecatedForceAscii; 295 final RichInputMethodSubtype keyboardSubtype = (forceAscii && !asciiCapable) 296 ? RichInputMethodSubtype.getNoLanguageSubtype() 297 : subtype; 298 mParams.mSubtype = keyboardSubtype; 299 mParams.mKeyboardLayoutSetName = KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX 300 + keyboardSubtype.getKeyboardLayoutSetName(); 301 return this; 302 } 303 setIsSpellChecker(final boolean isSpellChecker)304 public Builder setIsSpellChecker(final boolean isSpellChecker) { 305 mParams.mIsSpellChecker = isSpellChecker; 306 return this; 307 } 308 setVoiceInputKeyEnabled(final boolean enabled)309 public Builder setVoiceInputKeyEnabled(final boolean enabled) { 310 mParams.mVoiceInputKeyEnabled = enabled; 311 return this; 312 } 313 setLanguageSwitchKeyEnabled(final boolean enabled)314 public Builder setLanguageSwitchKeyEnabled(final boolean enabled) { 315 mParams.mLanguageSwitchKeyEnabled = enabled; 316 return this; 317 } 318 disableTouchPositionCorrectionData()319 public Builder disableTouchPositionCorrectionData() { 320 mParams.mDisableTouchPositionCorrectionDataForTest = true; 321 return this; 322 } 323 setSplitLayoutEnabledByUser(final boolean enabled)324 public Builder setSplitLayoutEnabledByUser(final boolean enabled) { 325 mParams.mIsSplitLayoutEnabledByUser = enabled; 326 return this; 327 } 328 329 // Super redux version of reading the script ID for some subtype from Xml. readScriptId(final Resources resources, final InputMethodSubtype subtype)330 static int readScriptId(final Resources resources, 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 readScriptIdFromTagFeature(final Resources resources, final XmlPullParser parser)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 build()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 getXmlId(final Resources resources, final String keyboardLayoutSetName)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 parseKeyboardLayoutSet(final Resources res, final int resId)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 parseKeyboardLayoutSetContent(final XmlPullParser parser)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 mParams.mScriptId = readScriptIdFromTagFeature(mResources, 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 parseKeyboardLayoutSetElement(final XmlPullParser parser)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 getKeyboardMode(final EditorInfo editorInfo)462 private static int getKeyboardMode(final EditorInfo editorInfo) { 463 final int inputType = editorInfo.inputType; 464 final int variation = inputType & InputType.TYPE_MASK_VARIATION; 465 466 switch (inputType & InputType.TYPE_MASK_CLASS) { 467 case InputType.TYPE_CLASS_NUMBER: 468 return KeyboardId.MODE_NUMBER; 469 case InputType.TYPE_CLASS_DATETIME: 470 switch (variation) { 471 case InputType.TYPE_DATETIME_VARIATION_DATE: 472 return KeyboardId.MODE_DATE; 473 case InputType.TYPE_DATETIME_VARIATION_TIME: 474 return KeyboardId.MODE_TIME; 475 default: // InputType.TYPE_DATETIME_VARIATION_NORMAL 476 return KeyboardId.MODE_DATETIME; 477 } 478 case InputType.TYPE_CLASS_PHONE: 479 return KeyboardId.MODE_PHONE; 480 case InputType.TYPE_CLASS_TEXT: 481 if (InputTypeUtils.isEmailVariation(variation)) { 482 return KeyboardId.MODE_EMAIL; 483 } else if (variation == InputType.TYPE_TEXT_VARIATION_URI) { 484 return KeyboardId.MODE_URL; 485 } else if (variation == InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE) { 486 return KeyboardId.MODE_IM; 487 } else if (variation == InputType.TYPE_TEXT_VARIATION_FILTER) { 488 return KeyboardId.MODE_TEXT; 489 } else { 490 return KeyboardId.MODE_TEXT; 491 } 492 default: 493 return KeyboardId.MODE_TEXT; 494 } 495 } 496 } 497 } 498