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