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_MICROPHONE; 21 import static com.android.inputmethod.latin.Constants.ImeOption.NO_MICROPHONE_COMPAT; 22 import static com.android.inputmethod.latin.Constants.ImeOption.NO_SETTINGS_KEY; 23 import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.ASCII_CAPABLE; 24 25 import android.content.Context; 26 import android.content.res.Configuration; 27 import android.content.res.Resources; 28 import android.content.res.TypedArray; 29 import android.content.res.XmlResourceParser; 30 import android.text.InputType; 31 import android.text.TextUtils; 32 import android.util.DisplayMetrics; 33 import android.util.Log; 34 import android.util.SparseArray; 35 import android.util.Xml; 36 import android.view.inputmethod.EditorInfo; 37 import android.view.inputmethod.InputMethodSubtype; 38 39 import com.android.inputmethod.compat.EditorInfoCompatUtils; 40 import com.android.inputmethod.keyboard.internal.KeyboardBuilder; 41 import com.android.inputmethod.keyboard.internal.KeyboardParams; 42 import com.android.inputmethod.keyboard.internal.KeysCache; 43 import com.android.inputmethod.latin.AdditionalSubtype; 44 import com.android.inputmethod.latin.CollectionUtils; 45 import com.android.inputmethod.latin.InputAttributes; 46 import com.android.inputmethod.latin.InputTypeUtils; 47 import com.android.inputmethod.latin.LatinImeLogger; 48 import com.android.inputmethod.latin.R; 49 import com.android.inputmethod.latin.ResourceUtils; 50 import com.android.inputmethod.latin.SubtypeLocale; 51 import com.android.inputmethod.latin.SubtypeSwitcher; 52 import com.android.inputmethod.latin.XmlParseUtils; 53 54 import org.xmlpull.v1.XmlPullParser; 55 import org.xmlpull.v1.XmlPullParserException; 56 57 import java.io.IOException; 58 import java.lang.ref.SoftReference; 59 import java.util.HashMap; 60 61 /** 62 * This class represents a set of keyboard layouts. Each of them represents a different keyboard 63 * specific to a keyboard state, such as alphabet, symbols, and so on. Layouts in the same 64 * {@link KeyboardLayoutSet} are related to each other. 65 * A {@link KeyboardLayoutSet} needs to be created for each 66 * {@link android.view.inputmethod.EditorInfo}. 67 */ 68 public final class KeyboardLayoutSet { 69 private static final String TAG = KeyboardLayoutSet.class.getSimpleName(); 70 private static final boolean DEBUG_CACHE = LatinImeLogger.sDBG; 71 72 private static final String TAG_KEYBOARD_SET = "KeyboardLayoutSet"; 73 private static final String TAG_ELEMENT = "Element"; 74 75 private static final String KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX = "keyboard_layout_set_"; 76 private static final int SPELLCHECKER_DUMMY_KEYBOARD_WIDTH = 480; 77 private static final int SPELLCHECKER_DUMMY_KEYBOARD_HEIGHT = 800; 78 79 private final Context mContext; 80 private final Params mParams; 81 82 private static final HashMap<KeyboardId, SoftReference<Keyboard>> sKeyboardCache = 83 CollectionUtils.newHashMap(); 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 EditorInfo mEditorInfo; 106 boolean mDisableTouchPositionCorrectionDataForTest; 107 boolean mVoiceKeyEnabled; 108 boolean mVoiceKeyOnMain; 109 boolean mNoSettingsKey; 110 boolean mLanguageSwitchKeyEnabled; 111 InputMethodSubtype mSubtype; 112 int mOrientation; 113 int mKeyboardWidth; 114 int mKeyboardHeight; 115 // Sparse array of KeyboardLayoutSet element parameters indexed by element's id. 116 final SparseArray<ElementParams> mKeyboardLayoutSetElementIdToParamsMap = 117 CollectionUtils.newSparseArray(); 118 } 119 120 public static void clearKeyboardCache() { 121 sKeyboardCache.clear(); 122 sKeysCache.clear(); 123 } 124 125 KeyboardLayoutSet(final Context context, final Params params) { 126 mContext = context; 127 mParams = params; 128 } 129 130 public Keyboard getKeyboard(final int baseKeyboardLayoutSetElementId) { 131 final int keyboardLayoutSetElementId; 132 switch (mParams.mMode) { 133 case KeyboardId.MODE_PHONE: 134 if (baseKeyboardLayoutSetElementId == KeyboardId.ELEMENT_SYMBOLS) { 135 keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE_SYMBOLS; 136 } else { 137 keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE; 138 } 139 break; 140 case KeyboardId.MODE_NUMBER: 141 case KeyboardId.MODE_DATE: 142 case KeyboardId.MODE_TIME: 143 case KeyboardId.MODE_DATETIME: 144 keyboardLayoutSetElementId = KeyboardId.ELEMENT_NUMBER; 145 break; 146 default: 147 keyboardLayoutSetElementId = baseKeyboardLayoutSetElementId; 148 break; 149 } 150 151 ElementParams elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get( 152 keyboardLayoutSetElementId); 153 if (elementParams == null) { 154 elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get( 155 KeyboardId.ELEMENT_ALPHABET); 156 } 157 // Note: The keyboard for each shift state, and mode are represented as an elementName 158 // attribute in a keyboard_layout_set XML file. Also each keyboard layout XML resource is 159 // specified as an elementKeyboard attribute in the file. 160 // The KeyboardId is an internal key for a Keyboard object. 161 final KeyboardId id = new KeyboardId(keyboardLayoutSetElementId, mParams); 162 try { 163 return getKeyboard(elementParams, id); 164 } catch (RuntimeException e) { 165 throw new KeyboardLayoutSetException(e, id); 166 } 167 } 168 169 private Keyboard getKeyboard(final ElementParams elementParams, final KeyboardId id) { 170 final SoftReference<Keyboard> ref = sKeyboardCache.get(id); 171 Keyboard keyboard = (ref == null) ? null : ref.get(); 172 if (keyboard == null) { 173 final KeyboardBuilder<KeyboardParams> builder = 174 new KeyboardBuilder<KeyboardParams>(mContext, new KeyboardParams()); 175 if (id.isAlphabetKeyboard()) { 176 builder.setAutoGenerate(sKeysCache); 177 } 178 final int keyboardXmlId = elementParams.mKeyboardXmlId; 179 builder.load(keyboardXmlId, id); 180 if (mParams.mDisableTouchPositionCorrectionDataForTest) { 181 builder.disableTouchPositionCorrectionDataForTest(); 182 } 183 builder.setProximityCharsCorrectionEnabled( 184 elementParams.mProximityCharsCorrectionEnabled); 185 keyboard = builder.build(); 186 sKeyboardCache.put(id, new SoftReference<Keyboard>(keyboard)); 187 188 if (DEBUG_CACHE) { 189 Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": " 190 + ((ref == null) ? "LOAD" : "GCed") + " id=" + id); 191 } 192 } else if (DEBUG_CACHE) { 193 Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": HIT id=" + id); 194 } 195 196 return keyboard; 197 } 198 199 public static final class Builder { 200 private final Context mContext; 201 private final String mPackageName; 202 private final Resources mResources; 203 private final EditorInfo mEditorInfo; 204 205 private final Params mParams = new Params(); 206 207 private static final EditorInfo EMPTY_EDITOR_INFO = new EditorInfo(); 208 209 public Builder(final Context context, final EditorInfo editorInfo) { 210 mContext = context; 211 mPackageName = context.getPackageName(); 212 mResources = context.getResources(); 213 mEditorInfo = editorInfo; 214 final Params params = mParams; 215 216 params.mMode = getKeyboardMode(editorInfo); 217 params.mEditorInfo = (editorInfo != null) ? editorInfo : EMPTY_EDITOR_INFO; 218 params.mNoSettingsKey = InputAttributes.inPrivateImeOptions( 219 mPackageName, NO_SETTINGS_KEY, mEditorInfo); 220 } 221 222 public Builder setScreenGeometry(final int widthPixels, final int heightPixels) { 223 final Params params = mParams; 224 params.mOrientation = (heightPixels > widthPixels) 225 ? Configuration.ORIENTATION_PORTRAIT : Configuration.ORIENTATION_LANDSCAPE; 226 setDefaultKeyboardSize(widthPixels, heightPixels); 227 return this; 228 } 229 230 private void setDefaultKeyboardSize(final int widthPixels, final int heightPixels) { 231 final String keyboardHeightString = ResourceUtils.getDeviceOverrideValue( 232 mResources, R.array.keyboard_heights); 233 final float keyboardHeight; 234 if (TextUtils.isEmpty(keyboardHeightString)) { 235 keyboardHeight = mResources.getDimension(R.dimen.keyboardHeight); 236 } else { 237 keyboardHeight = Float.parseFloat(keyboardHeightString) 238 * mResources.getDisplayMetrics().density; 239 } 240 final float maxKeyboardHeight = mResources.getFraction( 241 R.fraction.maxKeyboardHeight, heightPixels, heightPixels); 242 float minKeyboardHeight = mResources.getFraction( 243 R.fraction.minKeyboardHeight, heightPixels, heightPixels); 244 if (minKeyboardHeight < 0.0f) { 245 // Specified fraction was negative, so it should be calculated against display 246 // width. 247 minKeyboardHeight = -mResources.getFraction( 248 R.fraction.minKeyboardHeight, widthPixels, widthPixels); 249 } 250 // Keyboard height will not exceed maxKeyboardHeight and will not be less than 251 // minKeyboardHeight. 252 mParams.mKeyboardHeight = (int)Math.max( 253 Math.min(keyboardHeight, maxKeyboardHeight), minKeyboardHeight); 254 mParams.mKeyboardWidth = widthPixels; 255 } 256 257 public Builder setSubtype(final InputMethodSubtype subtype) { 258 final boolean asciiCapable = subtype.containsExtraValueKey(ASCII_CAPABLE); 259 @SuppressWarnings("deprecation") 260 final boolean deprecatedForceAscii = InputAttributes.inPrivateImeOptions( 261 mPackageName, FORCE_ASCII, 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 + SubtypeLocale.getKeyboardLayoutSetName(keyboardSubtype); 271 return this; 272 } 273 274 public Builder setOptions(final boolean voiceKeyEnabled, final boolean voiceKeyOnMain, 275 final boolean languageSwitchKeyEnabled) { 276 @SuppressWarnings("deprecation") 277 final boolean deprecatedNoMicrophone = InputAttributes.inPrivateImeOptions( 278 null, NO_MICROPHONE_COMPAT, mEditorInfo); 279 final boolean noMicrophone = InputAttributes.inPrivateImeOptions( 280 mPackageName, NO_MICROPHONE, mEditorInfo) 281 || deprecatedNoMicrophone; 282 mParams.mVoiceKeyEnabled = voiceKeyEnabled && !noMicrophone; 283 mParams.mVoiceKeyOnMain = voiceKeyOnMain; 284 mParams.mLanguageSwitchKeyEnabled = languageSwitchKeyEnabled; 285 return this; 286 } 287 288 public void disableTouchPositionCorrectionData() { 289 mParams.mDisableTouchPositionCorrectionDataForTest = true; 290 } 291 292 public KeyboardLayoutSet build() { 293 if (mParams.mOrientation == Configuration.ORIENTATION_UNDEFINED) 294 throw new RuntimeException("Screen geometry is not specified"); 295 if (mParams.mSubtype == null) 296 throw new RuntimeException("KeyboardLayoutSet subtype is not specified"); 297 final String packageName = mResources.getResourcePackageName( 298 R.xml.keyboard_layout_set_qwerty); 299 final String keyboardLayoutSetName = mParams.mKeyboardLayoutSetName; 300 final int xmlId = mResources.getIdentifier(keyboardLayoutSetName, "xml", packageName); 301 try { 302 parseKeyboardLayoutSet(mResources, xmlId); 303 } catch (final IOException e) { 304 throw new RuntimeException(e.getMessage() + " in " + keyboardLayoutSetName, e); 305 } catch (final XmlPullParserException e) { 306 throw new RuntimeException(e.getMessage() + " in " + keyboardLayoutSetName, e); 307 } 308 return new KeyboardLayoutSet(mContext, mParams); 309 } 310 311 private void parseKeyboardLayoutSet(final Resources res, final int resId) 312 throws XmlPullParserException, IOException { 313 final XmlResourceParser parser = res.getXml(resId); 314 try { 315 while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { 316 final int event = parser.next(); 317 if (event == XmlPullParser.START_TAG) { 318 final String tag = parser.getName(); 319 if (TAG_KEYBOARD_SET.equals(tag)) { 320 parseKeyboardLayoutSetContent(parser); 321 } else { 322 throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD_SET); 323 } 324 } 325 } 326 } finally { 327 parser.close(); 328 } 329 } 330 331 private void parseKeyboardLayoutSetContent(final XmlPullParser parser) 332 throws XmlPullParserException, IOException { 333 while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { 334 final int event = parser.next(); 335 if (event == XmlPullParser.START_TAG) { 336 final String tag = parser.getName(); 337 if (TAG_ELEMENT.equals(tag)) { 338 parseKeyboardLayoutSetElement(parser); 339 } else { 340 throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD_SET); 341 } 342 } else if (event == XmlPullParser.END_TAG) { 343 final String tag = parser.getName(); 344 if (TAG_KEYBOARD_SET.equals(tag)) { 345 break; 346 } else { 347 throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_KEYBOARD_SET); 348 } 349 } 350 } 351 } 352 353 private void parseKeyboardLayoutSetElement(final XmlPullParser parser) 354 throws XmlPullParserException, IOException { 355 final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser), 356 R.styleable.KeyboardLayoutSet_Element); 357 try { 358 XmlParseUtils.checkAttributeExists(a, 359 R.styleable.KeyboardLayoutSet_Element_elementName, "elementName", 360 TAG_ELEMENT, parser); 361 XmlParseUtils.checkAttributeExists(a, 362 R.styleable.KeyboardLayoutSet_Element_elementKeyboard, "elementKeyboard", 363 TAG_ELEMENT, parser); 364 XmlParseUtils.checkEndTag(TAG_ELEMENT, parser); 365 366 final ElementParams elementParams = new ElementParams(); 367 final int elementName = a.getInt( 368 R.styleable.KeyboardLayoutSet_Element_elementName, 0); 369 elementParams.mKeyboardXmlId = a.getResourceId( 370 R.styleable.KeyboardLayoutSet_Element_elementKeyboard, 0); 371 elementParams.mProximityCharsCorrectionEnabled = a.getBoolean( 372 R.styleable.KeyboardLayoutSet_Element_enableProximityCharsCorrection, 373 false); 374 mParams.mKeyboardLayoutSetElementIdToParamsMap.put(elementName, elementParams); 375 } finally { 376 a.recycle(); 377 } 378 } 379 380 private static int getKeyboardMode(final EditorInfo editorInfo) { 381 if (editorInfo == null) 382 return KeyboardId.MODE_TEXT; 383 384 final int inputType = editorInfo.inputType; 385 final int variation = inputType & InputType.TYPE_MASK_VARIATION; 386 387 switch (inputType & InputType.TYPE_MASK_CLASS) { 388 case InputType.TYPE_CLASS_NUMBER: 389 return KeyboardId.MODE_NUMBER; 390 case InputType.TYPE_CLASS_DATETIME: 391 switch (variation) { 392 case InputType.TYPE_DATETIME_VARIATION_DATE: 393 return KeyboardId.MODE_DATE; 394 case InputType.TYPE_DATETIME_VARIATION_TIME: 395 return KeyboardId.MODE_TIME; 396 default: // InputType.TYPE_DATETIME_VARIATION_NORMAL 397 return KeyboardId.MODE_DATETIME; 398 } 399 case InputType.TYPE_CLASS_PHONE: 400 return KeyboardId.MODE_PHONE; 401 case InputType.TYPE_CLASS_TEXT: 402 if (InputTypeUtils.isEmailVariation(variation)) { 403 return KeyboardId.MODE_EMAIL; 404 } else if (variation == InputType.TYPE_TEXT_VARIATION_URI) { 405 return KeyboardId.MODE_URL; 406 } else if (variation == InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE) { 407 return KeyboardId.MODE_IM; 408 } else if (variation == InputType.TYPE_TEXT_VARIATION_FILTER) { 409 return KeyboardId.MODE_TEXT; 410 } else { 411 return KeyboardId.MODE_TEXT; 412 } 413 default: 414 return KeyboardId.MODE_TEXT; 415 } 416 } 417 } 418 419 public static KeyboardLayoutSet createKeyboardSetForSpellChecker(final Context context, 420 final String locale, final String layout) { 421 final InputMethodSubtype subtype = 422 AdditionalSubtype.createAdditionalSubtype(locale, layout, null); 423 return createKeyboardSet(context, subtype, SPELLCHECKER_DUMMY_KEYBOARD_WIDTH, 424 SPELLCHECKER_DUMMY_KEYBOARD_HEIGHT, false); 425 } 426 427 public static KeyboardLayoutSet createKeyboardSetForTest(final Context context, 428 final InputMethodSubtype subtype, final int orientation, 429 final boolean testCasesHaveTouchCoordinates) { 430 final DisplayMetrics dm = context.getResources().getDisplayMetrics(); 431 final int width; 432 final int height; 433 if (orientation == Configuration.ORIENTATION_LANDSCAPE) { 434 width = Math.max(dm.widthPixels, dm.heightPixels); 435 height = Math.min(dm.widthPixels, dm.heightPixels); 436 } else if (orientation == Configuration.ORIENTATION_PORTRAIT) { 437 width = Math.min(dm.widthPixels, dm.heightPixels); 438 height = Math.max(dm.widthPixels, dm.heightPixels); 439 } else { 440 throw new RuntimeException("Orientation should be ORIENTATION_LANDSCAPE or " 441 + "ORIENTATION_PORTRAIT: orientation=" + orientation); 442 } 443 return createKeyboardSet(context, subtype, width, height, testCasesHaveTouchCoordinates); 444 } 445 446 private static KeyboardLayoutSet createKeyboardSet(final Context context, 447 final InputMethodSubtype subtype, final int width, final int height, 448 final boolean testCasesHaveTouchCoordinates) { 449 final EditorInfo editorInfo = new EditorInfo(); 450 editorInfo.inputType = InputType.TYPE_CLASS_TEXT; 451 final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder( 452 context, editorInfo); 453 builder.setScreenGeometry(width, height); 454 builder.setSubtype(subtype); 455 if (!testCasesHaveTouchCoordinates) { 456 // For spell checker and tests 457 builder.disableTouchPositionCorrectionData(); 458 } 459 return builder.build(); 460 } 461 } 462