KeyboardLayoutSet.java revision a410cb48eab0cd75aa27e20f60e47a29a59fb9ff
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.Configuration; 27import android.content.res.Resources; 28import android.content.res.TypedArray; 29import android.content.res.XmlResourceParser; 30import android.text.InputType; 31import android.text.TextUtils; 32import android.util.DisplayMetrics; 33import android.util.Log; 34import android.util.SparseArray; 35import android.util.Xml; 36import android.view.inputmethod.EditorInfo; 37import android.view.inputmethod.InputMethodSubtype; 38 39import com.android.inputmethod.annotations.UsedForTesting; 40import com.android.inputmethod.compat.EditorInfoCompatUtils; 41import com.android.inputmethod.keyboard.internal.KeyboardBuilder; 42import com.android.inputmethod.keyboard.internal.KeyboardParams; 43import com.android.inputmethod.keyboard.internal.KeysCache; 44import com.android.inputmethod.latin.InputAttributes; 45import com.android.inputmethod.latin.LatinImeLogger; 46import com.android.inputmethod.latin.R; 47import com.android.inputmethod.latin.SubtypeSwitcher; 48import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils; 49import com.android.inputmethod.latin.utils.CollectionUtils; 50import com.android.inputmethod.latin.utils.InputTypeUtils; 51import com.android.inputmethod.latin.utils.ResourceUtils; 52import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; 53import com.android.inputmethod.latin.utils.XmlParseUtils; 54 55import org.xmlpull.v1.XmlPullParser; 56import org.xmlpull.v1.XmlPullParserException; 57 58import java.io.IOException; 59import java.lang.ref.SoftReference; 60import java.util.HashMap; 61 62/** 63 * This class represents a set of keyboard layouts. Each of them represents a different keyboard 64 * specific to a keyboard state, such as alphabet, symbols, and so on. Layouts in the same 65 * {@link KeyboardLayoutSet} are related to each other. 66 * A {@link KeyboardLayoutSet} needs to be created for each 67 * {@link android.view.inputmethod.EditorInfo}. 68 */ 69public final class KeyboardLayoutSet { 70 private static final String TAG = KeyboardLayoutSet.class.getSimpleName(); 71 private static final boolean DEBUG_CACHE = LatinImeLogger.sDBG; 72 73 private static final String TAG_KEYBOARD_SET = "KeyboardLayoutSet"; 74 private static final String TAG_ELEMENT = "Element"; 75 76 private static final String KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX = "keyboard_layout_set_"; 77 private static final int SPELLCHECKER_DUMMY_KEYBOARD_WIDTH = 480; 78 private static final int SPELLCHECKER_DUMMY_KEYBOARD_HEIGHT = 800; 79 80 private final Context mContext; 81 private final Params mParams; 82 83 // How many layouts we forcibly keep in cache. This only includes ALPHABET (default) and 84 // ALPHABET_AUTOMATIC_SHIFTED layouts - other layouts may stay in memory in the map of 85 // soft-references, but we forcibly cache this many alphabetic/auto-shifted layouts. 86 private static final int FORCIBLE_CACHE_SIZE = 4; 87 // By construction of soft references, anything that is also referenced somewhere else 88 // will stay in the cache. So we forcibly keep some references in an array to prevent 89 // them from disappearing from sKeyboardCache. 90 private static final Keyboard[] sForcibleKeyboardCache = new Keyboard[FORCIBLE_CACHE_SIZE]; 91 private static final HashMap<KeyboardId, SoftReference<Keyboard>> sKeyboardCache = 92 CollectionUtils.newHashMap(); 93 private static final KeysCache sKeysCache = new KeysCache(); 94 95 @SuppressWarnings("serial") 96 public static final class KeyboardLayoutSetException extends RuntimeException { 97 public final KeyboardId mKeyboardId; 98 99 public KeyboardLayoutSetException(final Throwable cause, final KeyboardId keyboardId) { 100 super(cause); 101 mKeyboardId = keyboardId; 102 } 103 } 104 105 private static final class ElementParams { 106 int mKeyboardXmlId; 107 boolean mProximityCharsCorrectionEnabled; 108 public ElementParams() {} 109 } 110 111 public static final class Params { 112 String mKeyboardLayoutSetName; 113 int mMode; 114 EditorInfo mEditorInfo; 115 boolean mDisableTouchPositionCorrectionDataForTest; 116 boolean mVoiceKeyEnabled; 117 boolean mVoiceKeyOnMain; 118 boolean mNoSettingsKey; 119 boolean mLanguageSwitchKeyEnabled; 120 InputMethodSubtype mSubtype; 121 boolean mIsSpellChecker; 122 int mOrientation; 123 int mKeyboardWidth; 124 int mKeyboardHeight; 125 // Sparse array of KeyboardLayoutSet element parameters indexed by element's id. 126 final SparseArray<ElementParams> mKeyboardLayoutSetElementIdToParamsMap = 127 CollectionUtils.newSparseArray(); 128 } 129 130 public 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 (RuntimeException e) { 175 throw new KeyboardLayoutSetException(e, id); 176 } 177 } 178 179 private Keyboard getKeyboard(final ElementParams elementParams, final KeyboardId id) { 180 final SoftReference<Keyboard> ref = sKeyboardCache.get(id); 181 Keyboard keyboard = (ref == null) ? null : ref.get(); 182 if (keyboard == null) { 183 final KeyboardBuilder<KeyboardParams> builder = 184 new KeyboardBuilder<KeyboardParams>(mContext, new KeyboardParams()); 185 if (id.isAlphabetKeyboard()) { 186 builder.setAutoGenerate(sKeysCache); 187 } 188 final int keyboardXmlId = elementParams.mKeyboardXmlId; 189 builder.load(keyboardXmlId, id); 190 if (mParams.mDisableTouchPositionCorrectionDataForTest) { 191 builder.disableTouchPositionCorrectionDataForTest(); 192 } 193 builder.setProximityCharsCorrectionEnabled( 194 elementParams.mProximityCharsCorrectionEnabled); 195 keyboard = builder.build(); 196 sKeyboardCache.put(id, new SoftReference<Keyboard>(keyboard)); 197 if ((id.mElementId == KeyboardId.ELEMENT_ALPHABET 198 || id.mElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED) 199 && !mParams.mIsSpellChecker) { 200 // We only forcibly cache the primary, "ALPHABET", layouts. 201 for (int i = sForcibleKeyboardCache.length - 1; i >= 1; --i) { 202 sForcibleKeyboardCache[i] = sForcibleKeyboardCache[i - 1]; 203 } 204 sForcibleKeyboardCache[0] = keyboard; 205 if (DEBUG_CACHE) { 206 Log.d(TAG, "forcing caching of keyboard with id=" + id); 207 } 208 } 209 if (DEBUG_CACHE) { 210 Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": " 211 + ((ref == null) ? "LOAD" : "GCed") + " id=" + id); 212 } 213 } else if (DEBUG_CACHE) { 214 Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": HIT id=" + id); 215 } 216 217 return keyboard; 218 } 219 220 public static final class Builder { 221 private final Context mContext; 222 private final String mPackageName; 223 private final Resources mResources; 224 private final EditorInfo mEditorInfo; 225 226 private final Params mParams = new Params(); 227 228 private static final EditorInfo EMPTY_EDITOR_INFO = new EditorInfo(); 229 230 public Builder(final Context context, final EditorInfo editorInfo) { 231 mContext = context; 232 mPackageName = context.getPackageName(); 233 mResources = context.getResources(); 234 mEditorInfo = editorInfo; 235 final Params params = mParams; 236 237 params.mMode = getKeyboardMode(editorInfo); 238 params.mEditorInfo = (editorInfo != null) ? editorInfo : EMPTY_EDITOR_INFO; 239 params.mNoSettingsKey = InputAttributes.inPrivateImeOptions( 240 mPackageName, NO_SETTINGS_KEY, mEditorInfo); 241 } 242 243 public Builder setScreenGeometry(final int widthPixels, final int heightPixels) { 244 final Params params = mParams; 245 params.mOrientation = (heightPixels > widthPixels) 246 ? Configuration.ORIENTATION_PORTRAIT : Configuration.ORIENTATION_LANDSCAPE; 247 setDefaultKeyboardSize(widthPixels, heightPixels); 248 return this; 249 } 250 251 private void setDefaultKeyboardSize(final int widthPixels, final int heightPixels) { 252 final String keyboardHeightString = ResourceUtils.getDeviceOverrideValue( 253 mResources, R.array.keyboard_heights); 254 final float keyboardHeight; 255 if (TextUtils.isEmpty(keyboardHeightString)) { 256 keyboardHeight = mResources.getDimension(R.dimen.keyboardHeight); 257 } else { 258 keyboardHeight = Float.parseFloat(keyboardHeightString) 259 * mResources.getDisplayMetrics().density; 260 } 261 final float maxKeyboardHeight = mResources.getFraction( 262 R.fraction.maxKeyboardHeight, heightPixels, heightPixels); 263 float minKeyboardHeight = mResources.getFraction( 264 R.fraction.minKeyboardHeight, heightPixels, heightPixels); 265 if (minKeyboardHeight < 0.0f) { 266 // Specified fraction was negative, so it should be calculated against display 267 // width. 268 minKeyboardHeight = -mResources.getFraction( 269 R.fraction.minKeyboardHeight, widthPixels, widthPixels); 270 } 271 // Keyboard height will not exceed maxKeyboardHeight and will not be less than 272 // minKeyboardHeight. 273 mParams.mKeyboardHeight = (int)Math.max( 274 Math.min(keyboardHeight, maxKeyboardHeight), minKeyboardHeight); 275 mParams.mKeyboardWidth = widthPixels; 276 } 277 278 public Builder setSubtype(final InputMethodSubtype subtype) { 279 final boolean asciiCapable = subtype.containsExtraValueKey(ASCII_CAPABLE); 280 @SuppressWarnings("deprecation") 281 final boolean deprecatedForceAscii = InputAttributes.inPrivateImeOptions( 282 mPackageName, FORCE_ASCII, mEditorInfo); 283 final boolean forceAscii = EditorInfoCompatUtils.hasFlagForceAscii( 284 mParams.mEditorInfo.imeOptions) 285 || deprecatedForceAscii; 286 final InputMethodSubtype keyboardSubtype = (forceAscii && !asciiCapable) 287 ? SubtypeSwitcher.getInstance().getNoLanguageSubtype() 288 : subtype; 289 mParams.mSubtype = keyboardSubtype; 290 mParams.mKeyboardLayoutSetName = KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX 291 + SubtypeLocaleUtils.getKeyboardLayoutSetName(keyboardSubtype); 292 return this; 293 } 294 295 public Builder setIsSpellChecker(final boolean isSpellChecker) { 296 mParams.mIsSpellChecker = true; 297 return this; 298 } 299 300 public Builder setOptions(final boolean voiceKeyEnabled, final boolean voiceKeyOnMain, 301 final boolean languageSwitchKeyEnabled) { 302 @SuppressWarnings("deprecation") 303 final boolean deprecatedNoMicrophone = InputAttributes.inPrivateImeOptions( 304 null, NO_MICROPHONE_COMPAT, mEditorInfo); 305 final boolean noMicrophone = InputAttributes.inPrivateImeOptions( 306 mPackageName, NO_MICROPHONE, mEditorInfo) 307 || deprecatedNoMicrophone; 308 mParams.mVoiceKeyEnabled = voiceKeyEnabled && !noMicrophone; 309 mParams.mVoiceKeyOnMain = voiceKeyOnMain; 310 mParams.mLanguageSwitchKeyEnabled = languageSwitchKeyEnabled; 311 return this; 312 } 313 314 public void disableTouchPositionCorrectionData() { 315 mParams.mDisableTouchPositionCorrectionDataForTest = true; 316 } 317 318 public KeyboardLayoutSet build() { 319 if (mParams.mOrientation == Configuration.ORIENTATION_UNDEFINED) 320 throw new RuntimeException("Screen geometry is not specified"); 321 if (mParams.mSubtype == null) 322 throw new RuntimeException("KeyboardLayoutSet subtype is not specified"); 323 final String packageName = mResources.getResourcePackageName( 324 R.xml.keyboard_layout_set_qwerty); 325 final String keyboardLayoutSetName = mParams.mKeyboardLayoutSetName; 326 final int xmlId = mResources.getIdentifier(keyboardLayoutSetName, "xml", packageName); 327 try { 328 parseKeyboardLayoutSet(mResources, xmlId); 329 } catch (final IOException e) { 330 throw new RuntimeException(e.getMessage() + " in " + keyboardLayoutSetName, e); 331 } catch (final XmlPullParserException e) { 332 throw new RuntimeException(e.getMessage() + " in " + keyboardLayoutSetName, e); 333 } 334 return new KeyboardLayoutSet(mContext, mParams); 335 } 336 337 private void parseKeyboardLayoutSet(final Resources res, final int resId) 338 throws XmlPullParserException, IOException { 339 final XmlResourceParser parser = res.getXml(resId); 340 try { 341 while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { 342 final int event = parser.next(); 343 if (event == XmlPullParser.START_TAG) { 344 final String tag = parser.getName(); 345 if (TAG_KEYBOARD_SET.equals(tag)) { 346 parseKeyboardLayoutSetContent(parser); 347 } else { 348 throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD_SET); 349 } 350 } 351 } 352 } finally { 353 parser.close(); 354 } 355 } 356 357 private void parseKeyboardLayoutSetContent(final XmlPullParser parser) 358 throws XmlPullParserException, IOException { 359 while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { 360 final int event = parser.next(); 361 if (event == XmlPullParser.START_TAG) { 362 final String tag = parser.getName(); 363 if (TAG_ELEMENT.equals(tag)) { 364 parseKeyboardLayoutSetElement(parser); 365 } else { 366 throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD_SET); 367 } 368 } else if (event == XmlPullParser.END_TAG) { 369 final String tag = parser.getName(); 370 if (TAG_KEYBOARD_SET.equals(tag)) { 371 break; 372 } else { 373 throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_KEYBOARD_SET); 374 } 375 } 376 } 377 } 378 379 private void parseKeyboardLayoutSetElement(final XmlPullParser parser) 380 throws XmlPullParserException, IOException { 381 final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser), 382 R.styleable.KeyboardLayoutSet_Element); 383 try { 384 XmlParseUtils.checkAttributeExists(a, 385 R.styleable.KeyboardLayoutSet_Element_elementName, "elementName", 386 TAG_ELEMENT, parser); 387 XmlParseUtils.checkAttributeExists(a, 388 R.styleable.KeyboardLayoutSet_Element_elementKeyboard, "elementKeyboard", 389 TAG_ELEMENT, parser); 390 XmlParseUtils.checkEndTag(TAG_ELEMENT, parser); 391 392 final ElementParams elementParams = new ElementParams(); 393 final int elementName = a.getInt( 394 R.styleable.KeyboardLayoutSet_Element_elementName, 0); 395 elementParams.mKeyboardXmlId = a.getResourceId( 396 R.styleable.KeyboardLayoutSet_Element_elementKeyboard, 0); 397 elementParams.mProximityCharsCorrectionEnabled = a.getBoolean( 398 R.styleable.KeyboardLayoutSet_Element_enableProximityCharsCorrection, 399 false); 400 mParams.mKeyboardLayoutSetElementIdToParamsMap.put(elementName, elementParams); 401 } finally { 402 a.recycle(); 403 } 404 } 405 406 private static int getKeyboardMode(final EditorInfo editorInfo) { 407 if (editorInfo == null) 408 return KeyboardId.MODE_TEXT; 409 410 final int inputType = editorInfo.inputType; 411 final int variation = inputType & InputType.TYPE_MASK_VARIATION; 412 413 switch (inputType & InputType.TYPE_MASK_CLASS) { 414 case InputType.TYPE_CLASS_NUMBER: 415 return KeyboardId.MODE_NUMBER; 416 case InputType.TYPE_CLASS_DATETIME: 417 switch (variation) { 418 case InputType.TYPE_DATETIME_VARIATION_DATE: 419 return KeyboardId.MODE_DATE; 420 case InputType.TYPE_DATETIME_VARIATION_TIME: 421 return KeyboardId.MODE_TIME; 422 default: // InputType.TYPE_DATETIME_VARIATION_NORMAL 423 return KeyboardId.MODE_DATETIME; 424 } 425 case InputType.TYPE_CLASS_PHONE: 426 return KeyboardId.MODE_PHONE; 427 case InputType.TYPE_CLASS_TEXT: 428 if (InputTypeUtils.isEmailVariation(variation)) { 429 return KeyboardId.MODE_EMAIL; 430 } else if (variation == InputType.TYPE_TEXT_VARIATION_URI) { 431 return KeyboardId.MODE_URL; 432 } else if (variation == InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE) { 433 return KeyboardId.MODE_IM; 434 } else if (variation == InputType.TYPE_TEXT_VARIATION_FILTER) { 435 return KeyboardId.MODE_TEXT; 436 } else { 437 return KeyboardId.MODE_TEXT; 438 } 439 default: 440 return KeyboardId.MODE_TEXT; 441 } 442 } 443 } 444 445 public static KeyboardLayoutSet createKeyboardSetForSpellChecker(final Context context, 446 final String locale, final String layout) { 447 final InputMethodSubtype subtype = 448 AdditionalSubtypeUtils.createAdditionalSubtype(locale, layout, null); 449 return createKeyboardSet(context, subtype, SPELLCHECKER_DUMMY_KEYBOARD_WIDTH, 450 SPELLCHECKER_DUMMY_KEYBOARD_HEIGHT, false /* testCasesHaveTouchCoordinates */, 451 true /* isSpellChecker */); 452 } 453 454 @UsedForTesting 455 public static KeyboardLayoutSet createKeyboardSetForTest(final Context context, 456 final InputMethodSubtype subtype, final int orientation, 457 final boolean testCasesHaveTouchCoordinates) { 458 final DisplayMetrics dm = context.getResources().getDisplayMetrics(); 459 final int width; 460 final int height; 461 if (orientation == Configuration.ORIENTATION_LANDSCAPE) { 462 width = Math.max(dm.widthPixels, dm.heightPixels); 463 height = Math.min(dm.widthPixels, dm.heightPixels); 464 } else if (orientation == Configuration.ORIENTATION_PORTRAIT) { 465 width = Math.min(dm.widthPixels, dm.heightPixels); 466 height = Math.max(dm.widthPixels, dm.heightPixels); 467 } else { 468 throw new RuntimeException("Orientation should be ORIENTATION_LANDSCAPE or " 469 + "ORIENTATION_PORTRAIT: orientation=" + orientation); 470 } 471 return createKeyboardSet(context, subtype, width, height, testCasesHaveTouchCoordinates, 472 false /* isSpellChecker */); 473 } 474 475 private static KeyboardLayoutSet createKeyboardSet(final Context context, 476 final InputMethodSubtype subtype, final int width, final int height, 477 final boolean testCasesHaveTouchCoordinates, final boolean isSpellChecker) { 478 final EditorInfo editorInfo = new EditorInfo(); 479 editorInfo.inputType = InputType.TYPE_CLASS_TEXT; 480 final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder( 481 context, editorInfo); 482 builder.setScreenGeometry(width, height); 483 builder.setSubtype(subtype); 484 builder.setIsSpellChecker(isSpellChecker); 485 if (!testCasesHaveTouchCoordinates) { 486 // For spell checker and tests 487 builder.disableTouchPositionCorrectionData(); 488 } 489 return builder.build(); 490 } 491} 492