• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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