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 KeyboardLayoutSetException(final Throwable cause, final KeyboardId keyboardId)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; ElementParams()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 clearKeyboardCache()120 public static void clearKeyboardCache() { 121 sKeyboardCache.clear(); 122 sKeysCache.clear(); 123 } 124 KeyboardLayoutSet(final Context context, final Params params)125 KeyboardLayoutSet(final Context context, final Params params) { 126 mContext = context; 127 mParams = params; 128 } 129 getKeyboard(final int baseKeyboardLayoutSetElementId)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 getKeyboard(final ElementParams elementParams, final KeyboardId id)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 Builder(final Context context, final EditorInfo editorInfo)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 setScreenGeometry(final int widthPixels, final int heightPixels)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 setDefaultKeyboardSize(final int widthPixels, final int heightPixels)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 setSubtype(final InputMethodSubtype subtype)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 setOptions(final boolean voiceKeyEnabled, final boolean voiceKeyOnMain, final boolean languageSwitchKeyEnabled)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 disableTouchPositionCorrectionData()288 public void disableTouchPositionCorrectionData() { 289 mParams.mDisableTouchPositionCorrectionDataForTest = true; 290 } 291 build()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 parseKeyboardLayoutSet(final Resources res, final int resId)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 parseKeyboardLayoutSetContent(final XmlPullParser parser)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 parseKeyboardLayoutSetElement(final XmlPullParser parser)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 getKeyboardMode(final EditorInfo editorInfo)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 createKeyboardSetForSpellChecker(final Context context, final String locale, final String layout)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 createKeyboardSetForTest(final Context context, final InputMethodSubtype subtype, final int orientation, final boolean testCasesHaveTouchCoordinates)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 createKeyboardSet(final Context context, final InputMethodSubtype subtype, final int width, final int height, final boolean testCasesHaveTouchCoordinates)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