KeyboardLayoutSet.java revision ecfbf4625c8afd9cde7b79e0c7846b87e20f79e9
1/* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * 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, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * 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.Configuration; 27import android.content.res.Resources; 28import android.content.res.TypedArray; 29import android.content.res.XmlResourceParser; 30import android.text.InputType; 31import android.util.Log; 32import android.util.SparseArray; 33import android.util.Xml; 34import android.view.inputmethod.EditorInfo; 35import android.view.inputmethod.InputMethodSubtype; 36 37import com.android.inputmethod.compat.EditorInfoCompatUtils; 38import com.android.inputmethod.keyboard.KeyboardLayoutSet.Params.ElementParams; 39import com.android.inputmethod.latin.CollectionUtils; 40import com.android.inputmethod.latin.InputAttributes; 41import com.android.inputmethod.latin.InputTypeUtils; 42import com.android.inputmethod.latin.LatinImeLogger; 43import com.android.inputmethod.latin.R; 44import com.android.inputmethod.latin.SubtypeLocale; 45import com.android.inputmethod.latin.SubtypeSwitcher; 46import com.android.inputmethod.latin.XmlParseUtils; 47 48import org.xmlpull.v1.XmlPullParser; 49import org.xmlpull.v1.XmlPullParserException; 50 51import java.io.IOException; 52import java.lang.ref.SoftReference; 53import java.util.HashMap; 54 55/** 56 * This class represents a set of keyboard layouts. Each of them represents a different keyboard 57 * specific to a keyboard state, such as alphabet, symbols, and so on. Layouts in the same 58 * {@link KeyboardLayoutSet} are related to each other. 59 * A {@link KeyboardLayoutSet} needs to be created for each 60 * {@link android.view.inputmethod.EditorInfo}. 61 */ 62public class KeyboardLayoutSet { 63 private static final String TAG = KeyboardLayoutSet.class.getSimpleName(); 64 private static final boolean DEBUG_CACHE = LatinImeLogger.sDBG; 65 66 private static final String TAG_KEYBOARD_SET = "KeyboardLayoutSet"; 67 private static final String TAG_ELEMENT = "Element"; 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 private static final HashMap<KeyboardId, SoftReference<Keyboard>> sKeyboardCache = 75 CollectionUtils.newHashMap(); 76 private static final KeysCache sKeysCache = new KeysCache(); 77 78 public static class KeyboardLayoutSetException extends RuntimeException { 79 public final KeyboardId mKeyboardId; 80 81 public KeyboardLayoutSetException(Throwable cause, KeyboardId keyboardId) { 82 super(cause); 83 mKeyboardId = keyboardId; 84 } 85 } 86 87 public static class KeysCache { 88 private final HashMap<Key, Key> mMap = CollectionUtils.newHashMap(); 89 90 public void clear() { 91 mMap.clear(); 92 } 93 94 public Key get(Key key) { 95 final Key existingKey = mMap.get(key); 96 if (existingKey != null) { 97 // Reuse the existing element that equals to "key" without adding "key" to the map. 98 return existingKey; 99 } 100 mMap.put(key, key); 101 return key; 102 } 103 } 104 105 static class Params { 106 String mKeyboardLayoutSetName; 107 int mMode; 108 EditorInfo mEditorInfo; 109 boolean mTouchPositionCorrectionEnabled; 110 boolean mVoiceKeyEnabled; 111 boolean mVoiceKeyOnMain; 112 boolean mNoSettingsKey; 113 boolean mLanguageSwitchKeyEnabled; 114 InputMethodSubtype mSubtype; 115 int mDeviceFormFactor; 116 int mOrientation; 117 int mWidth; 118 // Sparse array of KeyboardLayoutSet element parameters indexed by element's id. 119 final SparseArray<ElementParams> mKeyboardLayoutSetElementIdToParamsMap = 120 CollectionUtils.newSparseArray(); 121 122 static class ElementParams { 123 int mKeyboardXmlId; 124 boolean mProximityCharsCorrectionEnabled; 125 } 126 } 127 128 public static void clearKeyboardCache() { 129 sKeyboardCache.clear(); 130 sKeysCache.clear(); 131 } 132 133 private KeyboardLayoutSet(Context context, Params params) { 134 mContext = context; 135 mParams = params; 136 } 137 138 public Keyboard getKeyboard(int baseKeyboardLayoutSetElementId) { 139 final int keyboardLayoutSetElementId; 140 switch (mParams.mMode) { 141 case KeyboardId.MODE_PHONE: 142 if (baseKeyboardLayoutSetElementId == KeyboardId.ELEMENT_SYMBOLS) { 143 keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE_SYMBOLS; 144 } else { 145 keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE; 146 } 147 break; 148 case KeyboardId.MODE_NUMBER: 149 case KeyboardId.MODE_DATE: 150 case KeyboardId.MODE_TIME: 151 case KeyboardId.MODE_DATETIME: 152 keyboardLayoutSetElementId = KeyboardId.ELEMENT_NUMBER; 153 break; 154 default: 155 keyboardLayoutSetElementId = baseKeyboardLayoutSetElementId; 156 break; 157 } 158 159 ElementParams elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get( 160 keyboardLayoutSetElementId); 161 if (elementParams == null) { 162 elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get( 163 KeyboardId.ELEMENT_ALPHABET); 164 } 165 final KeyboardId id = getKeyboardId(keyboardLayoutSetElementId); 166 try { 167 return getKeyboard(elementParams, id); 168 } catch (RuntimeException e) { 169 throw new KeyboardLayoutSetException(e, id); 170 } 171 } 172 173 private Keyboard getKeyboard(ElementParams elementParams, final KeyboardId id) { 174 final SoftReference<Keyboard> ref = sKeyboardCache.get(id); 175 Keyboard keyboard = (ref == null) ? null : ref.get(); 176 if (keyboard == null) { 177 final Keyboard.Builder<Keyboard.Params> builder = 178 new Keyboard.Builder<Keyboard.Params>(mContext, new Keyboard.Params()); 179 if (id.isAlphabetKeyboard()) { 180 builder.setAutoGenerate(sKeysCache); 181 } 182 final int keyboardXmlId = elementParams.mKeyboardXmlId; 183 builder.load(keyboardXmlId, id); 184 builder.setTouchPositionCorrectionEnabled(mParams.mTouchPositionCorrectionEnabled); 185 builder.setProximityCharsCorrectionEnabled( 186 elementParams.mProximityCharsCorrectionEnabled); 187 keyboard = builder.build(); 188 sKeyboardCache.put(id, new SoftReference<Keyboard>(keyboard)); 189 190 if (DEBUG_CACHE) { 191 Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": " 192 + ((ref == null) ? "LOAD" : "GCed") + " id=" + id); 193 } 194 } else if (DEBUG_CACHE) { 195 Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": HIT id=" + id); 196 } 197 198 return keyboard; 199 } 200 201 // Note: The keyboard for each locale, shift state, and mode are represented as 202 // KeyboardLayoutSet element id that is a key in keyboard_set.xml. Also that file specifies 203 // which XML layout should be used for each keyboard. The KeyboardId is an internal key for 204 // Keyboard object. 205 private KeyboardId getKeyboardId(int keyboardLayoutSetElementId) { 206 final Params params = mParams; 207 final boolean isSymbols = (keyboardLayoutSetElementId == KeyboardId.ELEMENT_SYMBOLS 208 || keyboardLayoutSetElementId == KeyboardId.ELEMENT_SYMBOLS_SHIFTED); 209 final boolean noLanguage = SubtypeLocale.isNoLanguage(params.mSubtype); 210 final boolean voiceKeyEnabled = params.mVoiceKeyEnabled && !noLanguage; 211 final boolean hasShortcutKey = voiceKeyEnabled && (isSymbols != params.mVoiceKeyOnMain); 212 return new KeyboardId(keyboardLayoutSetElementId, params.mSubtype, params.mDeviceFormFactor, 213 params.mOrientation, params.mWidth, params.mMode, params.mEditorInfo, 214 params.mNoSettingsKey, voiceKeyEnabled, hasShortcutKey, 215 params.mLanguageSwitchKeyEnabled); 216 } 217 218 public static class Builder { 219 private final Context mContext; 220 private final String mPackageName; 221 private final Resources mResources; 222 private final EditorInfo mEditorInfo; 223 224 private final Params mParams = new Params(); 225 226 private static final EditorInfo EMPTY_EDITOR_INFO = new EditorInfo(); 227 228 public Builder(Context context, EditorInfo editorInfo) { 229 mContext = context; 230 mPackageName = context.getPackageName(); 231 mResources = context.getResources(); 232 mEditorInfo = editorInfo; 233 final Params params = mParams; 234 235 params.mMode = getKeyboardMode(editorInfo); 236 params.mEditorInfo = (editorInfo != null) ? editorInfo : EMPTY_EDITOR_INFO; 237 params.mNoSettingsKey = InputAttributes.inPrivateImeOptions( 238 mPackageName, NO_SETTINGS_KEY, mEditorInfo); 239 } 240 241 public Builder setScreenGeometry(int deviceFormFactor, int orientation, int widthPixels) { 242 final Params params = mParams; 243 params.mDeviceFormFactor = deviceFormFactor; 244 params.mOrientation = orientation; 245 params.mWidth = widthPixels; 246 return this; 247 } 248 249 public Builder setSubtype(InputMethodSubtype subtype) { 250 final boolean asciiCapable = subtype.containsExtraValueKey(ASCII_CAPABLE); 251 @SuppressWarnings("deprecation") 252 final boolean deprecatedForceAscii = InputAttributes.inPrivateImeOptions( 253 mPackageName, FORCE_ASCII, mEditorInfo); 254 final boolean forceAscii = EditorInfoCompatUtils.hasFlagForceAscii( 255 mParams.mEditorInfo.imeOptions) 256 || deprecatedForceAscii; 257 final InputMethodSubtype keyboardSubtype = (forceAscii && !asciiCapable) 258 ? SubtypeSwitcher.getInstance().getNoLanguageSubtype() 259 : subtype; 260 mParams.mSubtype = keyboardSubtype; 261 mParams.mKeyboardLayoutSetName = KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX 262 + SubtypeLocale.getKeyboardLayoutSetName(keyboardSubtype); 263 return this; 264 } 265 266 public Builder setOptions(boolean voiceKeyEnabled, boolean voiceKeyOnMain, 267 boolean languageSwitchKeyEnabled) { 268 @SuppressWarnings("deprecation") 269 final boolean deprecatedNoMicrophone = InputAttributes.inPrivateImeOptions( 270 null, NO_MICROPHONE_COMPAT, mEditorInfo); 271 final boolean noMicrophone = InputAttributes.inPrivateImeOptions( 272 mPackageName, NO_MICROPHONE, mEditorInfo) 273 || deprecatedNoMicrophone; 274 mParams.mVoiceKeyEnabled = voiceKeyEnabled && !noMicrophone; 275 mParams.mVoiceKeyOnMain = voiceKeyOnMain; 276 mParams.mLanguageSwitchKeyEnabled = languageSwitchKeyEnabled; 277 return this; 278 } 279 280 public void setTouchPositionCorrectionEnabled(boolean enabled) { 281 mParams.mTouchPositionCorrectionEnabled = enabled; 282 } 283 284 public KeyboardLayoutSet build() { 285 if (mParams.mOrientation == Configuration.ORIENTATION_UNDEFINED) 286 throw new RuntimeException("Screen geometry is not specified"); 287 if (mParams.mSubtype == null) 288 throw new RuntimeException("KeyboardLayoutSet subtype is not specified"); 289 final String packageName = mResources.getResourcePackageName( 290 R.xml.keyboard_layout_set_qwerty); 291 final String keyboardLayoutSetName = mParams.mKeyboardLayoutSetName; 292 final int xmlId = mResources.getIdentifier(keyboardLayoutSetName, "xml", packageName); 293 try { 294 parseKeyboardLayoutSet(mResources, xmlId); 295 } catch (Exception e) { 296 throw new RuntimeException(e.getMessage() + " in " + keyboardLayoutSetName); 297 } 298 return new KeyboardLayoutSet(mContext, mParams); 299 } 300 301 private void parseKeyboardLayoutSet(Resources res, int resId) 302 throws XmlPullParserException, IOException { 303 final XmlResourceParser parser = res.getXml(resId); 304 try { 305 int event; 306 while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) { 307 if (event == XmlPullParser.START_TAG) { 308 final String tag = parser.getName(); 309 if (TAG_KEYBOARD_SET.equals(tag)) { 310 parseKeyboardLayoutSetContent(parser); 311 } else { 312 throw new XmlParseUtils.IllegalStartTag(parser, TAG_KEYBOARD_SET); 313 } 314 } 315 } 316 } finally { 317 parser.close(); 318 } 319 } 320 321 private void parseKeyboardLayoutSetContent(XmlPullParser parser) 322 throws XmlPullParserException, IOException { 323 int event; 324 while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) { 325 if (event == XmlPullParser.START_TAG) { 326 final String tag = parser.getName(); 327 if (TAG_ELEMENT.equals(tag)) { 328 parseKeyboardLayoutSetElement(parser); 329 } else { 330 throw new XmlParseUtils.IllegalStartTag(parser, TAG_KEYBOARD_SET); 331 } 332 } else if (event == XmlPullParser.END_TAG) { 333 final String tag = parser.getName(); 334 if (TAG_KEYBOARD_SET.equals(tag)) { 335 break; 336 } else { 337 throw new XmlParseUtils.IllegalEndTag(parser, TAG_KEYBOARD_SET); 338 } 339 } 340 } 341 } 342 343 private void parseKeyboardLayoutSetElement(XmlPullParser parser) 344 throws XmlPullParserException, IOException { 345 final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser), 346 R.styleable.KeyboardLayoutSet_Element); 347 try { 348 XmlParseUtils.checkAttributeExists(a, 349 R.styleable.KeyboardLayoutSet_Element_elementName, "elementName", 350 TAG_ELEMENT, parser); 351 XmlParseUtils.checkAttributeExists(a, 352 R.styleable.KeyboardLayoutSet_Element_elementKeyboard, "elementKeyboard", 353 TAG_ELEMENT, parser); 354 XmlParseUtils.checkEndTag(TAG_ELEMENT, parser); 355 356 final ElementParams elementParams = new ElementParams(); 357 final int elementName = a.getInt( 358 R.styleable.KeyboardLayoutSet_Element_elementName, 0); 359 elementParams.mKeyboardXmlId = a.getResourceId( 360 R.styleable.KeyboardLayoutSet_Element_elementKeyboard, 0); 361 elementParams.mProximityCharsCorrectionEnabled = a.getBoolean( 362 R.styleable.KeyboardLayoutSet_Element_enableProximityCharsCorrection, 363 false); 364 mParams.mKeyboardLayoutSetElementIdToParamsMap.put(elementName, elementParams); 365 } finally { 366 a.recycle(); 367 } 368 } 369 370 private static int getKeyboardMode(EditorInfo editorInfo) { 371 if (editorInfo == null) 372 return KeyboardId.MODE_TEXT; 373 374 final int inputType = editorInfo.inputType; 375 final int variation = inputType & InputType.TYPE_MASK_VARIATION; 376 377 switch (inputType & InputType.TYPE_MASK_CLASS) { 378 case InputType.TYPE_CLASS_NUMBER: 379 return KeyboardId.MODE_NUMBER; 380 case InputType.TYPE_CLASS_DATETIME: 381 switch (variation) { 382 case InputType.TYPE_DATETIME_VARIATION_DATE: 383 return KeyboardId.MODE_DATE; 384 case InputType.TYPE_DATETIME_VARIATION_TIME: 385 return KeyboardId.MODE_TIME; 386 default: // InputType.TYPE_DATETIME_VARIATION_NORMAL 387 return KeyboardId.MODE_DATETIME; 388 } 389 case InputType.TYPE_CLASS_PHONE: 390 return KeyboardId.MODE_PHONE; 391 case InputType.TYPE_CLASS_TEXT: 392 if (InputTypeUtils.isEmailVariation(variation)) { 393 return KeyboardId.MODE_EMAIL; 394 } else if (variation == InputType.TYPE_TEXT_VARIATION_URI) { 395 return KeyboardId.MODE_URL; 396 } else if (variation == InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE) { 397 return KeyboardId.MODE_IM; 398 } else if (variation == InputType.TYPE_TEXT_VARIATION_FILTER) { 399 return KeyboardId.MODE_TEXT; 400 } else { 401 return KeyboardId.MODE_TEXT; 402 } 403 default: 404 return KeyboardId.MODE_TEXT; 405 } 406 } 407 } 408} 409