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