KeyboardLayoutSet.java revision 7215e06e60d1fe106e47303af15e38bd23560d74
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_MICROPHONE; 21import static com.android.inputmethod.latin.Constants.ImeOption.NO_MICROPHONE_COMPAT; 22import static com.android.inputmethod.latin.Constants.ImeOption.NO_SETTINGS_KEY; 23import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.ASCII_CAPABLE; 24 25import android.content.Context; 26import android.content.res.Resources; 27import android.content.res.TypedArray; 28import android.content.res.XmlResourceParser; 29import android.text.InputType; 30import android.util.Log; 31import android.util.SparseArray; 32import android.util.Xml; 33import android.view.inputmethod.EditorInfo; 34import android.view.inputmethod.InputMethodSubtype; 35 36import com.android.inputmethod.compat.EditorInfoCompatUtils; 37import com.android.inputmethod.keyboard.internal.KeyboardBuilder; 38import com.android.inputmethod.keyboard.internal.KeyboardParams; 39import com.android.inputmethod.keyboard.internal.KeysCache; 40import com.android.inputmethod.latin.InputAttributes; 41import com.android.inputmethod.latin.LatinImeLogger; 42import com.android.inputmethod.latin.R; 43import com.android.inputmethod.latin.SubtypeSwitcher; 44import com.android.inputmethod.latin.utils.CollectionUtils; 45import com.android.inputmethod.latin.utils.InputTypeUtils; 46import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; 47import com.android.inputmethod.latin.utils.XmlParseUtils; 48 49import org.xmlpull.v1.XmlPullParser; 50import org.xmlpull.v1.XmlPullParserException; 51 52import java.io.IOException; 53import java.lang.ref.SoftReference; 54import java.util.HashMap; 55 56/** 57 * This class represents a set of keyboard layouts. Each of them represents a different keyboard 58 * specific to a keyboard state, such as alphabet, symbols, and so on. Layouts in the same 59 * {@link KeyboardLayoutSet} are related to each other. 60 * A {@link KeyboardLayoutSet} needs to be created for each 61 * {@link android.view.inputmethod.EditorInfo}. 62 */ 63public final class KeyboardLayoutSet { 64 private static final String TAG = KeyboardLayoutSet.class.getSimpleName(); 65 private static final boolean DEBUG_CACHE = LatinImeLogger.sDBG; 66 67 private static final String TAG_KEYBOARD_SET = "KeyboardLayoutSet"; 68 private static final String TAG_ELEMENT = "Element"; 69 70 private static final String KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX = "keyboard_layout_set_"; 71 72 private final Context mContext; 73 private final Params mParams; 74 75 // How many layouts we forcibly keep in cache. This only includes ALPHABET (default) and 76 // ALPHABET_AUTOMATIC_SHIFTED layouts - other layouts may stay in memory in the map of 77 // soft-references, but we forcibly cache this many alphabetic/auto-shifted layouts. 78 private static final int FORCIBLE_CACHE_SIZE = 4; 79 // By construction of soft references, anything that is also referenced somewhere else 80 // will stay in the cache. So we forcibly keep some references in an array to prevent 81 // them from disappearing from sKeyboardCache. 82 private static final Keyboard[] sForcibleKeyboardCache = new Keyboard[FORCIBLE_CACHE_SIZE]; 83 private static final HashMap<KeyboardId, SoftReference<Keyboard>> sKeyboardCache = 84 CollectionUtils.newHashMap(); 85 private static final KeysCache sKeysCache = new KeysCache(); 86 87 @SuppressWarnings("serial") 88 public static final class KeyboardLayoutSetException extends RuntimeException { 89 public final KeyboardId mKeyboardId; 90 91 public KeyboardLayoutSetException(final Throwable cause, final KeyboardId keyboardId) { 92 super(cause); 93 mKeyboardId = keyboardId; 94 } 95 } 96 97 private static final class ElementParams { 98 int mKeyboardXmlId; 99 boolean mProximityCharsCorrectionEnabled; 100 public ElementParams() {} 101 } 102 103 public static final class Params { 104 String mKeyboardLayoutSetName; 105 int mMode; 106 EditorInfo mEditorInfo; 107 boolean mDisableTouchPositionCorrectionDataForTest; 108 boolean mVoiceKeyEnabled; 109 boolean mVoiceKeyOnMain; 110 boolean mNoSettingsKey; 111 boolean mLanguageSwitchKeyEnabled; 112 InputMethodSubtype mSubtype; 113 boolean mIsSpellChecker; 114 int mKeyboardWidth; 115 int mKeyboardHeight; 116 // Sparse array of KeyboardLayoutSet element parameters indexed by element's id. 117 final SparseArray<ElementParams> mKeyboardLayoutSetElementIdToParamsMap = 118 CollectionUtils.newSparseArray(); 119 } 120 121 public static void clearKeyboardCache() { 122 sKeyboardCache.clear(); 123 sKeysCache.clear(); 124 } 125 126 KeyboardLayoutSet(final Context context, final Params params) { 127 mContext = context; 128 mParams = params; 129 } 130 131 public Keyboard getKeyboard(final int baseKeyboardLayoutSetElementId) { 132 final int keyboardLayoutSetElementId; 133 switch (mParams.mMode) { 134 case KeyboardId.MODE_PHONE: 135 if (baseKeyboardLayoutSetElementId == KeyboardId.ELEMENT_SYMBOLS) { 136 keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE_SYMBOLS; 137 } else { 138 keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE; 139 } 140 break; 141 case KeyboardId.MODE_NUMBER: 142 case KeyboardId.MODE_DATE: 143 case KeyboardId.MODE_TIME: 144 case KeyboardId.MODE_DATETIME: 145 keyboardLayoutSetElementId = KeyboardId.ELEMENT_NUMBER; 146 break; 147 default: 148 keyboardLayoutSetElementId = baseKeyboardLayoutSetElementId; 149 break; 150 } 151 152 ElementParams elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get( 153 keyboardLayoutSetElementId); 154 if (elementParams == null) { 155 elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get( 156 KeyboardId.ELEMENT_ALPHABET); 157 } 158 // Note: The keyboard for each shift state, and mode are represented as an elementName 159 // attribute in a keyboard_layout_set XML file. Also each keyboard layout XML resource is 160 // specified as an elementKeyboard attribute in the file. 161 // The KeyboardId is an internal key for a Keyboard object. 162 final KeyboardId id = new KeyboardId(keyboardLayoutSetElementId, mParams); 163 try { 164 return getKeyboard(elementParams, id); 165 } catch (RuntimeException e) { 166 throw new KeyboardLayoutSetException(e, id); 167 } 168 } 169 170 private Keyboard getKeyboard(final ElementParams elementParams, final KeyboardId id) { 171 final SoftReference<Keyboard> ref = sKeyboardCache.get(id); 172 final Keyboard cachedKeyboard = (ref == null) ? null : ref.get(); 173 if (cachedKeyboard != null) { 174 if (DEBUG_CACHE) { 175 Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": HIT id=" + id); 176 } 177 return cachedKeyboard; 178 } 179 180 final KeyboardBuilder<KeyboardParams> builder = 181 new KeyboardBuilder<KeyboardParams>(mContext, new KeyboardParams()); 182 if (id.isAlphabetKeyboard()) { 183 builder.setAutoGenerate(sKeysCache); 184 } 185 final int keyboardXmlId = elementParams.mKeyboardXmlId; 186 builder.load(keyboardXmlId, id); 187 if (mParams.mDisableTouchPositionCorrectionDataForTest) { 188 builder.disableTouchPositionCorrectionDataForTest(); 189 } 190 builder.setProximityCharsCorrectionEnabled(elementParams.mProximityCharsCorrectionEnabled); 191 final Keyboard keyboard = builder.build(); 192 sKeyboardCache.put(id, new SoftReference<Keyboard>(keyboard)); 193 if ((id.mElementId == KeyboardId.ELEMENT_ALPHABET 194 || id.mElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED) 195 && !mParams.mIsSpellChecker) { 196 // We only forcibly cache the primary, "ALPHABET", layouts. 197 for (int i = sForcibleKeyboardCache.length - 1; i >= 1; --i) { 198 sForcibleKeyboardCache[i] = sForcibleKeyboardCache[i - 1]; 199 } 200 sForcibleKeyboardCache[0] = keyboard; 201 if (DEBUG_CACHE) { 202 Log.d(TAG, "forcing caching of keyboard with id=" + id); 203 } 204 } 205 if (DEBUG_CACHE) { 206 Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": " 207 + ((ref == null) ? "LOAD" : "GCed") + " id=" + id); 208 } 209 return keyboard; 210 } 211 212 public static final class Builder { 213 private final Context mContext; 214 private final String mPackageName; 215 private final Resources mResources; 216 217 private final Params mParams = new Params(); 218 219 private static final EditorInfo EMPTY_EDITOR_INFO = new EditorInfo(); 220 221 public Builder(final Context context, final EditorInfo editorInfo) { 222 mContext = context; 223 mPackageName = context.getPackageName(); 224 mResources = context.getResources(); 225 final Params params = mParams; 226 227 params.mMode = getKeyboardMode(editorInfo); 228 params.mEditorInfo = (editorInfo != null) ? editorInfo : EMPTY_EDITOR_INFO; 229 params.mNoSettingsKey = InputAttributes.inPrivateImeOptions( 230 mPackageName, NO_SETTINGS_KEY, params.mEditorInfo); 231 } 232 233 public Builder setKeyboardGeometry(final int keyboardWidth, final int keyboardHeight) { 234 mParams.mKeyboardWidth = keyboardWidth; 235 mParams.mKeyboardHeight = keyboardHeight; 236 return this; 237 } 238 239 public Builder setSubtype(final InputMethodSubtype subtype) { 240 final boolean asciiCapable = subtype.containsExtraValueKey(ASCII_CAPABLE); 241 @SuppressWarnings("deprecation") 242 final boolean deprecatedForceAscii = InputAttributes.inPrivateImeOptions( 243 mPackageName, FORCE_ASCII, mParams.mEditorInfo); 244 final boolean forceAscii = EditorInfoCompatUtils.hasFlagForceAscii( 245 mParams.mEditorInfo.imeOptions) 246 || deprecatedForceAscii; 247 final InputMethodSubtype keyboardSubtype = (forceAscii && !asciiCapable) 248 ? SubtypeSwitcher.getInstance().getNoLanguageSubtype() 249 : subtype; 250 mParams.mSubtype = keyboardSubtype; 251 mParams.mKeyboardLayoutSetName = KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX 252 + SubtypeLocaleUtils.getKeyboardLayoutSetName(keyboardSubtype); 253 return this; 254 } 255 256 public Builder setIsSpellChecker(final boolean isSpellChecker) { 257 mParams.mIsSpellChecker = isSpellChecker; 258 return this; 259 } 260 261 public Builder setOptions(final boolean voiceKeyEnabled, final boolean voiceKeyOnMain, 262 final boolean languageSwitchKeyEnabled) { 263 @SuppressWarnings("deprecation") 264 final boolean deprecatedNoMicrophone = InputAttributes.inPrivateImeOptions( 265 null, NO_MICROPHONE_COMPAT, mParams.mEditorInfo); 266 final boolean noMicrophone = InputAttributes.inPrivateImeOptions( 267 mPackageName, NO_MICROPHONE, mParams.mEditorInfo) 268 || deprecatedNoMicrophone; 269 mParams.mVoiceKeyEnabled = voiceKeyEnabled && !noMicrophone; 270 mParams.mVoiceKeyOnMain = voiceKeyOnMain; 271 mParams.mLanguageSwitchKeyEnabled = languageSwitchKeyEnabled; 272 return this; 273 } 274 275 public void disableTouchPositionCorrectionData() { 276 mParams.mDisableTouchPositionCorrectionDataForTest = true; 277 } 278 279 public KeyboardLayoutSet build() { 280 if (mParams.mSubtype == null) 281 throw new RuntimeException("KeyboardLayoutSet subtype is not specified"); 282 final String packageName = mResources.getResourcePackageName( 283 R.xml.keyboard_layout_set_qwerty); 284 final String keyboardLayoutSetName = mParams.mKeyboardLayoutSetName; 285 final int xmlId = mResources.getIdentifier(keyboardLayoutSetName, "xml", packageName); 286 try { 287 parseKeyboardLayoutSet(mResources, xmlId); 288 } catch (final IOException e) { 289 throw new RuntimeException(e.getMessage() + " in " + keyboardLayoutSetName, e); 290 } catch (final XmlPullParserException e) { 291 throw new RuntimeException(e.getMessage() + " in " + keyboardLayoutSetName, e); 292 } 293 return new KeyboardLayoutSet(mContext, mParams); 294 } 295 296 private void parseKeyboardLayoutSet(final Resources res, final int resId) 297 throws XmlPullParserException, IOException { 298 final XmlResourceParser parser = res.getXml(resId); 299 try { 300 while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { 301 final int event = parser.next(); 302 if (event == XmlPullParser.START_TAG) { 303 final String tag = parser.getName(); 304 if (TAG_KEYBOARD_SET.equals(tag)) { 305 parseKeyboardLayoutSetContent(parser); 306 } else { 307 throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD_SET); 308 } 309 } 310 } 311 } finally { 312 parser.close(); 313 } 314 } 315 316 private void parseKeyboardLayoutSetContent(final XmlPullParser parser) 317 throws XmlPullParserException, IOException { 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_ELEMENT.equals(tag)) { 323 parseKeyboardLayoutSetElement(parser); 324 } else { 325 throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD_SET); 326 } 327 } else if (event == XmlPullParser.END_TAG) { 328 final String tag = parser.getName(); 329 if (TAG_KEYBOARD_SET.equals(tag)) { 330 break; 331 } else { 332 throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_KEYBOARD_SET); 333 } 334 } 335 } 336 } 337 338 private void parseKeyboardLayoutSetElement(final XmlPullParser parser) 339 throws XmlPullParserException, IOException { 340 final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser), 341 R.styleable.KeyboardLayoutSet_Element); 342 try { 343 XmlParseUtils.checkAttributeExists(a, 344 R.styleable.KeyboardLayoutSet_Element_elementName, "elementName", 345 TAG_ELEMENT, parser); 346 XmlParseUtils.checkAttributeExists(a, 347 R.styleable.KeyboardLayoutSet_Element_elementKeyboard, "elementKeyboard", 348 TAG_ELEMENT, parser); 349 XmlParseUtils.checkEndTag(TAG_ELEMENT, parser); 350 351 final ElementParams elementParams = new ElementParams(); 352 final int elementName = a.getInt( 353 R.styleable.KeyboardLayoutSet_Element_elementName, 0); 354 elementParams.mKeyboardXmlId = a.getResourceId( 355 R.styleable.KeyboardLayoutSet_Element_elementKeyboard, 0); 356 elementParams.mProximityCharsCorrectionEnabled = a.getBoolean( 357 R.styleable.KeyboardLayoutSet_Element_enableProximityCharsCorrection, 358 false); 359 mParams.mKeyboardLayoutSetElementIdToParamsMap.put(elementName, elementParams); 360 } finally { 361 a.recycle(); 362 } 363 } 364 365 private static int getKeyboardMode(final EditorInfo editorInfo) { 366 if (editorInfo == null) 367 return KeyboardId.MODE_TEXT; 368 369 final int inputType = editorInfo.inputType; 370 final int variation = inputType & InputType.TYPE_MASK_VARIATION; 371 372 switch (inputType & InputType.TYPE_MASK_CLASS) { 373 case InputType.TYPE_CLASS_NUMBER: 374 return KeyboardId.MODE_NUMBER; 375 case InputType.TYPE_CLASS_DATETIME: 376 switch (variation) { 377 case InputType.TYPE_DATETIME_VARIATION_DATE: 378 return KeyboardId.MODE_DATE; 379 case InputType.TYPE_DATETIME_VARIATION_TIME: 380 return KeyboardId.MODE_TIME; 381 default: // InputType.TYPE_DATETIME_VARIATION_NORMAL 382 return KeyboardId.MODE_DATETIME; 383 } 384 case InputType.TYPE_CLASS_PHONE: 385 return KeyboardId.MODE_PHONE; 386 case InputType.TYPE_CLASS_TEXT: 387 if (InputTypeUtils.isEmailVariation(variation)) { 388 return KeyboardId.MODE_EMAIL; 389 } else if (variation == InputType.TYPE_TEXT_VARIATION_URI) { 390 return KeyboardId.MODE_URL; 391 } else if (variation == InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE) { 392 return KeyboardId.MODE_IM; 393 } else if (variation == InputType.TYPE_TEXT_VARIATION_FILTER) { 394 return KeyboardId.MODE_TEXT; 395 } else { 396 return KeyboardId.MODE_TEXT; 397 } 398 default: 399 return KeyboardId.MODE_TEXT; 400 } 401 } 402 } 403} 404