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.Resources; 27import android.content.res.TypedArray; 28import android.content.res.XmlResourceParser; 29import android.text.InputType; 30import android.util.Log; 31import android.util.SparseArray; 32import android.util.Xml; 33import android.view.inputmethod.EditorInfo; 34import android.view.inputmethod.InputMethodSubtype; 35 36import com.android.inputmethod.compat.EditorInfoCompatUtils; 37import com.android.inputmethod.keyboard.internal.KeyboardBuilder; 38import com.android.inputmethod.keyboard.internal.KeyboardParams; 39import com.android.inputmethod.keyboard.internal.KeysCache; 40import com.android.inputmethod.latin.InputAttributes; 41import com.android.inputmethod.latin.LatinImeLogger; 42import com.android.inputmethod.latin.R; 43import com.android.inputmethod.latin.SubtypeSwitcher; 44import com.android.inputmethod.latin.utils.CollectionUtils; 45import com.android.inputmethod.latin.utils.InputTypeUtils; 46import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; 47import com.android.inputmethod.latin.utils.XmlParseUtils; 48 49import org.xmlpull.v1.XmlPullParser; 50import org.xmlpull.v1.XmlPullParserException; 51 52import java.io.IOException; 53import java.lang.ref.SoftReference; 54import java.util.HashMap; 55 56/** 57 * This class represents a set of keyboard layouts. Each of them represents a different keyboard 58 * specific to a keyboard state, such as alphabet, symbols, and so on. Layouts in the same 59 * {@link KeyboardLayoutSet} are related to each other. 60 * A {@link KeyboardLayoutSet} needs to be created for each 61 * {@link android.view.inputmethod.EditorInfo}. 62 */ 63public final class KeyboardLayoutSet { 64 private static final String TAG = KeyboardLayoutSet.class.getSimpleName(); 65 private static final boolean DEBUG_CACHE = LatinImeLogger.sDBG; 66 67 private static final String TAG_KEYBOARD_SET = "KeyboardLayoutSet"; 68 private static final String TAG_ELEMENT = "Element"; 69 70 private static final String KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX = "keyboard_layout_set_"; 71 72 private final Context mContext; 73 private final Params mParams; 74 75 // How many layouts we forcibly keep in cache. This only includes ALPHABET (default) and 76 // ALPHABET_AUTOMATIC_SHIFTED layouts - other layouts may stay in memory in the map of 77 // soft-references, but we forcibly cache this many alphabetic/auto-shifted layouts. 78 private static final int FORCIBLE_CACHE_SIZE = 4; 79 // By construction of soft references, anything that is also referenced somewhere else 80 // will stay in the cache. So we forcibly keep some references in an array to prevent 81 // them from disappearing from sKeyboardCache. 82 private static final Keyboard[] sForcibleKeyboardCache = new Keyboard[FORCIBLE_CACHE_SIZE]; 83 private static final HashMap<KeyboardId, SoftReference<Keyboard>> sKeyboardCache = 84 CollectionUtils.newHashMap(); 85 private static final KeysCache sKeysCache = new KeysCache(); 86 87 @SuppressWarnings("serial") 88 public static final class KeyboardLayoutSetException extends RuntimeException { 89 public final KeyboardId mKeyboardId; 90 91 public KeyboardLayoutSetException(final Throwable cause, final KeyboardId keyboardId) { 92 super(cause); 93 mKeyboardId = keyboardId; 94 } 95 } 96 97 private static final class ElementParams { 98 int mKeyboardXmlId; 99 boolean mProximityCharsCorrectionEnabled; 100 public ElementParams() {} 101 } 102 103 public static final class Params { 104 String mKeyboardLayoutSetName; 105 int mMode; 106 EditorInfo mEditorInfo; 107 boolean mDisableTouchPositionCorrectionDataForTest; 108 boolean mVoiceKeyEnabled; 109 // TODO: Remove mVoiceKeyOnMain when it's certainly confirmed that we no longer show 110 // the voice input key on the symbol layout 111 boolean mVoiceKeyOnMain; 112 boolean mNoSettingsKey; 113 boolean mLanguageSwitchKeyEnabled; 114 InputMethodSubtype mSubtype; 115 boolean mIsSpellChecker; 116 int mKeyboardWidth; 117 int mKeyboardHeight; 118 // Sparse array of KeyboardLayoutSet element parameters indexed by element's id. 119 final SparseArray<ElementParams> mKeyboardLayoutSetElementIdToParamsMap = 120 CollectionUtils.newSparseArray(); 121 } 122 123 public static void clearKeyboardCache() { 124 sKeyboardCache.clear(); 125 sKeysCache.clear(); 126 } 127 128 KeyboardLayoutSet(final Context context, final Params params) { 129 mContext = context; 130 mParams = params; 131 } 132 133 public Keyboard getKeyboard(final int baseKeyboardLayoutSetElementId) { 134 final int keyboardLayoutSetElementId; 135 switch (mParams.mMode) { 136 case KeyboardId.MODE_PHONE: 137 if (baseKeyboardLayoutSetElementId == KeyboardId.ELEMENT_SYMBOLS) { 138 keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE_SYMBOLS; 139 } else { 140 keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE; 141 } 142 break; 143 case KeyboardId.MODE_NUMBER: 144 case KeyboardId.MODE_DATE: 145 case KeyboardId.MODE_TIME: 146 case KeyboardId.MODE_DATETIME: 147 keyboardLayoutSetElementId = KeyboardId.ELEMENT_NUMBER; 148 break; 149 default: 150 keyboardLayoutSetElementId = baseKeyboardLayoutSetElementId; 151 break; 152 } 153 154 ElementParams elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get( 155 keyboardLayoutSetElementId); 156 if (elementParams == null) { 157 elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get( 158 KeyboardId.ELEMENT_ALPHABET); 159 } 160 // Note: The keyboard for each shift state, and mode are represented as an elementName 161 // attribute in a keyboard_layout_set XML file. Also each keyboard layout XML resource is 162 // specified as an elementKeyboard attribute in the file. 163 // The KeyboardId is an internal key for a Keyboard object. 164 final KeyboardId id = new KeyboardId(keyboardLayoutSetElementId, mParams); 165 try { 166 return getKeyboard(elementParams, id); 167 } catch (final RuntimeException e) { 168 Log.e(TAG, "Can't create keyboard: " + id, e); 169 throw new KeyboardLayoutSetException(e, id); 170 } 171 } 172 173 private Keyboard getKeyboard(final ElementParams elementParams, final KeyboardId id) { 174 final SoftReference<Keyboard> ref = sKeyboardCache.get(id); 175 final Keyboard cachedKeyboard = (ref == null) ? null : ref.get(); 176 if (cachedKeyboard != null) { 177 if (DEBUG_CACHE) { 178 Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": HIT id=" + id); 179 } 180 return cachedKeyboard; 181 } 182 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(elementParams.mProximityCharsCorrectionEnabled); 194 final Keyboard keyboard = builder.build(); 195 sKeyboardCache.put(id, new SoftReference<Keyboard>(keyboard)); 196 if ((id.mElementId == KeyboardId.ELEMENT_ALPHABET 197 || id.mElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED) 198 && !mParams.mIsSpellChecker) { 199 // We only forcibly cache the primary, "ALPHABET", layouts. 200 for (int i = sForcibleKeyboardCache.length - 1; i >= 1; --i) { 201 sForcibleKeyboardCache[i] = sForcibleKeyboardCache[i - 1]; 202 } 203 sForcibleKeyboardCache[0] = keyboard; 204 if (DEBUG_CACHE) { 205 Log.d(TAG, "forcing caching of keyboard with id=" + id); 206 } 207 } 208 if (DEBUG_CACHE) { 209 Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": " 210 + ((ref == null) ? "LOAD" : "GCed") + " id=" + id); 211 } 212 return keyboard; 213 } 214 215 public static final class Builder { 216 private final Context mContext; 217 private final String mPackageName; 218 private final Resources mResources; 219 220 private final Params mParams = new Params(); 221 222 private static final EditorInfo EMPTY_EDITOR_INFO = new EditorInfo(); 223 224 public Builder(final Context context, final EditorInfo editorInfo) { 225 mContext = context; 226 mPackageName = context.getPackageName(); 227 mResources = context.getResources(); 228 final Params params = mParams; 229 230 params.mMode = getKeyboardMode(editorInfo); 231 params.mEditorInfo = (editorInfo != null) ? editorInfo : EMPTY_EDITOR_INFO; 232 params.mNoSettingsKey = InputAttributes.inPrivateImeOptions( 233 mPackageName, NO_SETTINGS_KEY, params.mEditorInfo); 234 } 235 236 public Builder setKeyboardGeometry(final int keyboardWidth, final int keyboardHeight) { 237 mParams.mKeyboardWidth = keyboardWidth; 238 mParams.mKeyboardHeight = keyboardHeight; 239 return this; 240 } 241 242 public Builder setSubtype(final InputMethodSubtype subtype) { 243 final boolean asciiCapable = subtype.containsExtraValueKey(ASCII_CAPABLE); 244 @SuppressWarnings("deprecation") 245 final boolean deprecatedForceAscii = InputAttributes.inPrivateImeOptions( 246 mPackageName, FORCE_ASCII, mParams.mEditorInfo); 247 final boolean forceAscii = EditorInfoCompatUtils.hasFlagForceAscii( 248 mParams.mEditorInfo.imeOptions) 249 || deprecatedForceAscii; 250 final InputMethodSubtype keyboardSubtype = (forceAscii && !asciiCapable) 251 ? SubtypeSwitcher.getInstance().getNoLanguageSubtype() 252 : subtype; 253 mParams.mSubtype = keyboardSubtype; 254 mParams.mKeyboardLayoutSetName = KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX 255 + SubtypeLocaleUtils.getKeyboardLayoutSetName(keyboardSubtype); 256 return this; 257 } 258 259 public Builder setIsSpellChecker(final boolean isSpellChecker) { 260 mParams.mIsSpellChecker = isSpellChecker; 261 return this; 262 } 263 264 // TODO: Remove mVoiceKeyOnMain when it's certainly confirmed that we no longer show 265 // the voice input key on the symbol layout 266 public Builder setOptions(final boolean voiceKeyEnabled, final boolean voiceKeyOnMain, 267 final boolean languageSwitchKeyEnabled) { 268 @SuppressWarnings("deprecation") 269 final boolean deprecatedNoMicrophone = InputAttributes.inPrivateImeOptions( 270 null, NO_MICROPHONE_COMPAT, mParams.mEditorInfo); 271 final boolean noMicrophone = InputAttributes.inPrivateImeOptions( 272 mPackageName, NO_MICROPHONE, mParams.mEditorInfo) 273 || deprecatedNoMicrophone; 274 mParams.mVoiceKeyEnabled = voiceKeyEnabled && !noMicrophone; 275 mParams.mVoiceKeyOnMain = voiceKeyOnMain; 276 mParams.mLanguageSwitchKeyEnabled = languageSwitchKeyEnabled; 277 return this; 278 } 279 280 public void disableTouchPositionCorrectionData() { 281 mParams.mDisableTouchPositionCorrectionDataForTest = true; 282 } 283 284 public KeyboardLayoutSet build() { 285 if (mParams.mSubtype == null) 286 throw new RuntimeException("KeyboardLayoutSet subtype is not specified"); 287 final String packageName = mResources.getResourcePackageName( 288 R.xml.keyboard_layout_set_qwerty); 289 final String keyboardLayoutSetName = mParams.mKeyboardLayoutSetName; 290 final int xmlId = mResources.getIdentifier(keyboardLayoutSetName, "xml", packageName); 291 try { 292 parseKeyboardLayoutSet(mResources, xmlId); 293 } catch (final IOException e) { 294 throw new RuntimeException(e.getMessage() + " in " + keyboardLayoutSetName, e); 295 } catch (final XmlPullParserException e) { 296 throw new RuntimeException(e.getMessage() + " in " + keyboardLayoutSetName, e); 297 } 298 return new KeyboardLayoutSet(mContext, mParams); 299 } 300 301 private void parseKeyboardLayoutSet(final Resources res, final int resId) 302 throws XmlPullParserException, IOException { 303 final XmlResourceParser parser = res.getXml(resId); 304 try { 305 while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { 306 final int event = parser.next(); 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, TAG_KEYBOARD_SET); 313 } 314 } 315 } 316 } finally { 317 parser.close(); 318 } 319 } 320 321 private void parseKeyboardLayoutSetContent(final XmlPullParser parser) 322 throws XmlPullParserException, IOException { 323 while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { 324 final int event = parser.next(); 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, 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, TAG_KEYBOARD_SET); 338 } 339 } 340 } 341 } 342 343 private void parseKeyboardLayoutSetElement(final 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(final 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