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