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