• 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");
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.common.Constants.ImeOption.FORCE_ASCII;
20 import static com.android.inputmethod.latin.common.Constants.ImeOption.NO_SETTINGS_KEY;
21 
22 import android.content.Context;
23 import android.content.res.Resources;
24 import android.content.res.TypedArray;
25 import android.content.res.XmlResourceParser;
26 import android.text.InputType;
27 import android.util.Log;
28 import android.util.SparseArray;
29 import android.util.Xml;
30 import android.view.inputmethod.EditorInfo;
31 import android.view.inputmethod.InputMethodSubtype;
32 
33 import com.android.inputmethod.compat.EditorInfoCompatUtils;
34 import com.android.inputmethod.compat.InputMethodSubtypeCompatUtils;
35 import com.android.inputmethod.keyboard.internal.KeyboardBuilder;
36 import com.android.inputmethod.keyboard.internal.KeyboardParams;
37 import com.android.inputmethod.keyboard.internal.UniqueKeysCache;
38 import com.android.inputmethod.latin.InputAttributes;
39 import com.android.inputmethod.latin.R;
40 import com.android.inputmethod.latin.RichInputMethodSubtype;
41 import com.android.inputmethod.latin.define.DebugFlags;
42 import com.android.inputmethod.latin.utils.InputTypeUtils;
43 import com.android.inputmethod.latin.utils.ScriptUtils;
44 import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
45 import com.android.inputmethod.latin.utils.XmlParseUtils;
46 
47 import org.xmlpull.v1.XmlPullParser;
48 import org.xmlpull.v1.XmlPullParserException;
49 
50 import java.io.IOException;
51 import java.lang.ref.SoftReference;
52 import java.util.HashMap;
53 
54 import javax.annotation.Nonnull;
55 import javax.annotation.Nullable;
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 = false;
67 
68     private static final String TAG_KEYBOARD_SET = "KeyboardLayoutSet";
69     private static final String TAG_ELEMENT = "Element";
70     private static final String TAG_FEATURE = "Feature";
71 
72     private static final String KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX = "keyboard_layout_set_";
73 
74     private final Context mContext;
75     @Nonnull
76     private final Params mParams;
77 
78     // How many layouts we forcibly keep in cache. This only includes ALPHABET (default) and
79     // ALPHABET_AUTOMATIC_SHIFTED layouts - other layouts may stay in memory in the map of
80     // soft-references, but we forcibly cache this many alphabetic/auto-shifted layouts.
81     private static final int FORCIBLE_CACHE_SIZE = 4;
82     // By construction of soft references, anything that is also referenced somewhere else
83     // will stay in the cache. So we forcibly keep some references in an array to prevent
84     // them from disappearing from sKeyboardCache.
85     private static final Keyboard[] sForcibleKeyboardCache = new Keyboard[FORCIBLE_CACHE_SIZE];
86     private static final HashMap<KeyboardId, SoftReference<Keyboard>> sKeyboardCache =
87             new HashMap<>();
88     @Nonnull
89     private static final UniqueKeysCache sUniqueKeysCache = UniqueKeysCache.newInstance();
90     private final static HashMap<InputMethodSubtype, Integer> sScriptIdsForSubtypes =
91             new HashMap<>();
92 
93     @SuppressWarnings("serial")
94     public static final class KeyboardLayoutSetException extends RuntimeException {
95         public final KeyboardId mKeyboardId;
96 
KeyboardLayoutSetException(final Throwable cause, final KeyboardId keyboardId)97         public KeyboardLayoutSetException(final Throwable cause, final KeyboardId keyboardId) {
98             super(cause);
99             mKeyboardId = keyboardId;
100         }
101     }
102 
103     private static final class ElementParams {
104         int mKeyboardXmlId;
105         boolean mProximityCharsCorrectionEnabled;
106         boolean mSupportsSplitLayout;
107         boolean mAllowRedundantMoreKeys;
ElementParams()108         public ElementParams() {}
109     }
110 
111     public static final class Params {
112         String mKeyboardLayoutSetName;
113         int mMode;
114         boolean mDisableTouchPositionCorrectionDataForTest;
115         // TODO: Use {@link InputAttributes} instead of these variables.
116         EditorInfo mEditorInfo;
117         boolean mIsPasswordField;
118         boolean mVoiceInputKeyEnabled;
119         boolean mNoSettingsKey;
120         boolean mLanguageSwitchKeyEnabled;
121         RichInputMethodSubtype mSubtype;
122         boolean mIsSpellChecker;
123         int mKeyboardWidth;
124         int mKeyboardHeight;
125         int mScriptId = ScriptUtils.SCRIPT_LATIN;
126         // Indicates if the user has enabled the split-layout preference
127         // and the required ProductionFlags are enabled.
128         boolean mIsSplitLayoutEnabledByUser;
129         // Indicates if split layout is actually enabled, taking into account
130         // whether the user has enabled it, and the keyboard layout supports it.
131         boolean mIsSplitLayoutEnabled;
132         // Sparse array of KeyboardLayoutSet element parameters indexed by element's id.
133         final SparseArray<ElementParams> mKeyboardLayoutSetElementIdToParamsMap =
134                 new SparseArray<>();
135     }
136 
onSystemLocaleChanged()137     public static void onSystemLocaleChanged() {
138         clearKeyboardCache();
139     }
140 
onKeyboardThemeChanged()141     public static void onKeyboardThemeChanged() {
142         clearKeyboardCache();
143     }
144 
clearKeyboardCache()145     private static void clearKeyboardCache() {
146         sKeyboardCache.clear();
147         sUniqueKeysCache.clear();
148     }
149 
getScriptId(final Resources resources, @Nonnull final InputMethodSubtype subtype)150     public static int getScriptId(final Resources resources,
151             @Nonnull final InputMethodSubtype subtype) {
152         final Integer value = sScriptIdsForSubtypes.get(subtype);
153         if (null == value) {
154             final int scriptId = Builder.readScriptId(resources, subtype);
155             sScriptIdsForSubtypes.put(subtype, scriptId);
156             return scriptId;
157         }
158         return value;
159     }
160 
KeyboardLayoutSet(final Context context, @Nonnull final Params params)161     KeyboardLayoutSet(final Context context, @Nonnull final Params params) {
162         mContext = context;
163         mParams = params;
164     }
165 
166     @Nonnull
getKeyboard(final int baseKeyboardLayoutSetElementId)167     public Keyboard getKeyboard(final int baseKeyboardLayoutSetElementId) {
168         final int keyboardLayoutSetElementId;
169         switch (mParams.mMode) {
170         case KeyboardId.MODE_PHONE:
171             if (baseKeyboardLayoutSetElementId == KeyboardId.ELEMENT_SYMBOLS) {
172                 keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE_SYMBOLS;
173             } else {
174                 keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE;
175             }
176             break;
177         case KeyboardId.MODE_NUMBER:
178         case KeyboardId.MODE_DATE:
179         case KeyboardId.MODE_TIME:
180         case KeyboardId.MODE_DATETIME:
181             keyboardLayoutSetElementId = KeyboardId.ELEMENT_NUMBER;
182             break;
183         default:
184             keyboardLayoutSetElementId = baseKeyboardLayoutSetElementId;
185             break;
186         }
187 
188         ElementParams elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get(
189                 keyboardLayoutSetElementId);
190         if (elementParams == null) {
191             elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get(
192                     KeyboardId.ELEMENT_ALPHABET);
193         }
194         // Note: The keyboard for each shift state, and mode are represented as an elementName
195         // attribute in a keyboard_layout_set XML file.  Also each keyboard layout XML resource is
196         // specified as an elementKeyboard attribute in the file.
197         // The KeyboardId is an internal key for a Keyboard object.
198 
199         mParams.mIsSplitLayoutEnabled = mParams.mIsSplitLayoutEnabledByUser
200                 && elementParams.mSupportsSplitLayout;
201         final KeyboardId id = new KeyboardId(keyboardLayoutSetElementId, mParams);
202         try {
203             return getKeyboard(elementParams, id);
204         } catch (final RuntimeException e) {
205             Log.e(TAG, "Can't create keyboard: " + id, e);
206             throw new KeyboardLayoutSetException(e, id);
207         }
208     }
209 
210     @Nonnull
getKeyboard(final ElementParams elementParams, final KeyboardId id)211     private Keyboard getKeyboard(final ElementParams elementParams, final KeyboardId id) {
212         final SoftReference<Keyboard> ref = sKeyboardCache.get(id);
213         final Keyboard cachedKeyboard = (ref == null) ? null : ref.get();
214         if (cachedKeyboard != null) {
215             if (DEBUG_CACHE) {
216                 Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": HIT  id=" + id);
217             }
218             return cachedKeyboard;
219         }
220 
221         final KeyboardBuilder<KeyboardParams> builder =
222                 new KeyboardBuilder<>(mContext, new KeyboardParams(sUniqueKeysCache));
223         sUniqueKeysCache.setEnabled(id.isAlphabetKeyboard());
224         builder.setAllowRedundantMoreKes(elementParams.mAllowRedundantMoreKeys);
225         final int keyboardXmlId = elementParams.mKeyboardXmlId;
226         builder.load(keyboardXmlId, id);
227         if (mParams.mDisableTouchPositionCorrectionDataForTest) {
228             builder.disableTouchPositionCorrectionDataForTest();
229         }
230         builder.setProximityCharsCorrectionEnabled(elementParams.mProximityCharsCorrectionEnabled);
231         final Keyboard keyboard = builder.build();
232         sKeyboardCache.put(id, new SoftReference<>(keyboard));
233         if ((id.mElementId == KeyboardId.ELEMENT_ALPHABET
234                 || id.mElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED)
235                 && !mParams.mIsSpellChecker) {
236             // We only forcibly cache the primary, "ALPHABET", layouts.
237             for (int i = sForcibleKeyboardCache.length - 1; i >= 1; --i) {
238                 sForcibleKeyboardCache[i] = sForcibleKeyboardCache[i - 1];
239             }
240             sForcibleKeyboardCache[0] = keyboard;
241             if (DEBUG_CACHE) {
242                 Log.d(TAG, "forcing caching of keyboard with id=" + id);
243             }
244         }
245         if (DEBUG_CACHE) {
246             Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": "
247                     + ((ref == null) ? "LOAD" : "GCed") + " id=" + id);
248         }
249         return keyboard;
250     }
251 
getScriptId()252     public int getScriptId() {
253         return mParams.mScriptId;
254     }
255 
256     public static final class Builder {
257         private final Context mContext;
258         private final String mPackageName;
259         private final Resources mResources;
260 
261         private final Params mParams = new Params();
262 
263         private static final EditorInfo EMPTY_EDITOR_INFO = new EditorInfo();
264 
Builder(final Context context, @Nullable final EditorInfo ei)265         public Builder(final Context context, @Nullable final EditorInfo ei) {
266             mContext = context;
267             mPackageName = context.getPackageName();
268             mResources = context.getResources();
269             final Params params = mParams;
270 
271             final EditorInfo editorInfo = (ei != null) ? ei : EMPTY_EDITOR_INFO;
272             params.mMode = getKeyboardMode(editorInfo);
273             // TODO: Consolidate those with {@link InputAttributes}.
274             params.mEditorInfo = editorInfo;
275             params.mIsPasswordField = InputTypeUtils.isPasswordInputType(editorInfo.inputType);
276             params.mNoSettingsKey = InputAttributes.inPrivateImeOptions(
277                     mPackageName, NO_SETTINGS_KEY, editorInfo);
278         }
279 
setKeyboardGeometry(final int keyboardWidth, final int keyboardHeight)280         public Builder setKeyboardGeometry(final int keyboardWidth, final int keyboardHeight) {
281             mParams.mKeyboardWidth = keyboardWidth;
282             mParams.mKeyboardHeight = keyboardHeight;
283             return this;
284         }
285 
setSubtype(@onnull final RichInputMethodSubtype subtype)286         public Builder setSubtype(@Nonnull final RichInputMethodSubtype subtype) {
287             final boolean asciiCapable = InputMethodSubtypeCompatUtils.isAsciiCapable(subtype);
288             // TODO: Consolidate with {@link InputAttributes}.
289             @SuppressWarnings("deprecation")
290             final boolean deprecatedForceAscii = InputAttributes.inPrivateImeOptions(
291                     mPackageName, FORCE_ASCII, mParams.mEditorInfo);
292             final boolean forceAscii = EditorInfoCompatUtils.hasFlagForceAscii(
293                     mParams.mEditorInfo.imeOptions)
294                     || deprecatedForceAscii;
295             final RichInputMethodSubtype keyboardSubtype = (forceAscii && !asciiCapable)
296                     ? RichInputMethodSubtype.getNoLanguageSubtype()
297                     : subtype;
298             mParams.mSubtype = keyboardSubtype;
299             mParams.mKeyboardLayoutSetName = KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX
300                     + keyboardSubtype.getKeyboardLayoutSetName();
301             return this;
302         }
303 
setIsSpellChecker(final boolean isSpellChecker)304         public Builder setIsSpellChecker(final boolean isSpellChecker) {
305             mParams.mIsSpellChecker = isSpellChecker;
306             return this;
307         }
308 
setVoiceInputKeyEnabled(final boolean enabled)309         public Builder setVoiceInputKeyEnabled(final boolean enabled) {
310             mParams.mVoiceInputKeyEnabled = enabled;
311             return this;
312         }
313 
setLanguageSwitchKeyEnabled(final boolean enabled)314         public Builder setLanguageSwitchKeyEnabled(final boolean enabled) {
315             mParams.mLanguageSwitchKeyEnabled = enabled;
316             return this;
317         }
318 
disableTouchPositionCorrectionData()319         public Builder disableTouchPositionCorrectionData() {
320             mParams.mDisableTouchPositionCorrectionDataForTest = true;
321             return this;
322         }
323 
setSplitLayoutEnabledByUser(final boolean enabled)324         public Builder setSplitLayoutEnabledByUser(final boolean enabled) {
325             mParams.mIsSplitLayoutEnabledByUser = enabled;
326             return this;
327         }
328 
329         // Super redux version of reading the script ID for some subtype from Xml.
readScriptId(final Resources resources, final InputMethodSubtype subtype)330         static int readScriptId(final Resources resources, final InputMethodSubtype subtype) {
331             final String layoutSetName = KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX
332                     + SubtypeLocaleUtils.getKeyboardLayoutSetName(subtype);
333             final int xmlId = getXmlId(resources, layoutSetName);
334             final XmlResourceParser parser = resources.getXml(xmlId);
335             try {
336                 while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
337                     // Bovinate through the XML stupidly searching for TAG_FEATURE, and read
338                     // the script Id from it.
339                     parser.next();
340                     final String tag = parser.getName();
341                     if (TAG_FEATURE.equals(tag)) {
342                         return readScriptIdFromTagFeature(resources, parser);
343                     }
344                 }
345             } catch (final IOException | XmlPullParserException e) {
346                 throw new RuntimeException(e.getMessage() + " in " + layoutSetName, e);
347             } finally {
348                 parser.close();
349             }
350             // If the tag is not found, then the default script is Latin.
351             return ScriptUtils.SCRIPT_LATIN;
352         }
353 
readScriptIdFromTagFeature(final Resources resources, final XmlPullParser parser)354         private static int readScriptIdFromTagFeature(final Resources resources,
355                 final XmlPullParser parser) throws IOException, XmlPullParserException {
356             final TypedArray featureAttr = resources.obtainAttributes(Xml.asAttributeSet(parser),
357                     R.styleable.KeyboardLayoutSet_Feature);
358             try {
359                 final int scriptId =
360                         featureAttr.getInt(R.styleable.KeyboardLayoutSet_Feature_supportedScript,
361                                 ScriptUtils.SCRIPT_UNKNOWN);
362                 XmlParseUtils.checkEndTag(TAG_FEATURE, parser);
363                 return scriptId;
364             } finally {
365                 featureAttr.recycle();
366             }
367         }
368 
build()369         public KeyboardLayoutSet build() {
370             if (mParams.mSubtype == null)
371                 throw new RuntimeException("KeyboardLayoutSet subtype is not specified");
372             final int xmlId = getXmlId(mResources, mParams.mKeyboardLayoutSetName);
373             try {
374                 parseKeyboardLayoutSet(mResources, xmlId);
375             } catch (final IOException | XmlPullParserException e) {
376                 throw new RuntimeException(e.getMessage() + " in " + mParams.mKeyboardLayoutSetName,
377                         e);
378             }
379             return new KeyboardLayoutSet(mContext, mParams);
380         }
381 
getXmlId(final Resources resources, final String keyboardLayoutSetName)382         private static int getXmlId(final Resources resources, final String keyboardLayoutSetName) {
383             final String packageName = resources.getResourcePackageName(
384                     R.xml.keyboard_layout_set_qwerty);
385             return resources.getIdentifier(keyboardLayoutSetName, "xml", packageName);
386         }
387 
parseKeyboardLayoutSet(final Resources res, final int resId)388         private void parseKeyboardLayoutSet(final Resources res, final int resId)
389                 throws XmlPullParserException, IOException {
390             final XmlResourceParser parser = res.getXml(resId);
391             try {
392                 while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
393                     final int event = parser.next();
394                     if (event == XmlPullParser.START_TAG) {
395                         final String tag = parser.getName();
396                         if (TAG_KEYBOARD_SET.equals(tag)) {
397                             parseKeyboardLayoutSetContent(parser);
398                         } else {
399                             throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD_SET);
400                         }
401                     }
402                 }
403             } finally {
404                 parser.close();
405             }
406         }
407 
parseKeyboardLayoutSetContent(final XmlPullParser parser)408         private void parseKeyboardLayoutSetContent(final XmlPullParser parser)
409                 throws XmlPullParserException, IOException {
410             while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
411                 final int event = parser.next();
412                 if (event == XmlPullParser.START_TAG) {
413                     final String tag = parser.getName();
414                     if (TAG_ELEMENT.equals(tag)) {
415                         parseKeyboardLayoutSetElement(parser);
416                     } else if (TAG_FEATURE.equals(tag)) {
417                         mParams.mScriptId = readScriptIdFromTagFeature(mResources, parser);
418                     } else {
419                         throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD_SET);
420                     }
421                 } else if (event == XmlPullParser.END_TAG) {
422                     final String tag = parser.getName();
423                     if (TAG_KEYBOARD_SET.equals(tag)) {
424                         break;
425                     }
426                     throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_KEYBOARD_SET);
427                 }
428             }
429         }
430 
parseKeyboardLayoutSetElement(final XmlPullParser parser)431         private void parseKeyboardLayoutSetElement(final XmlPullParser parser)
432                 throws XmlPullParserException, IOException {
433             final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
434                     R.styleable.KeyboardLayoutSet_Element);
435             try {
436                 XmlParseUtils.checkAttributeExists(a,
437                         R.styleable.KeyboardLayoutSet_Element_elementName, "elementName",
438                         TAG_ELEMENT, parser);
439                 XmlParseUtils.checkAttributeExists(a,
440                         R.styleable.KeyboardLayoutSet_Element_elementKeyboard, "elementKeyboard",
441                         TAG_ELEMENT, parser);
442                 XmlParseUtils.checkEndTag(TAG_ELEMENT, parser);
443 
444                 final ElementParams elementParams = new ElementParams();
445                 final int elementName = a.getInt(
446                         R.styleable.KeyboardLayoutSet_Element_elementName, 0);
447                 elementParams.mKeyboardXmlId = a.getResourceId(
448                         R.styleable.KeyboardLayoutSet_Element_elementKeyboard, 0);
449                 elementParams.mProximityCharsCorrectionEnabled = a.getBoolean(
450                         R.styleable.KeyboardLayoutSet_Element_enableProximityCharsCorrection,
451                         false);
452                 elementParams.mSupportsSplitLayout = a.getBoolean(
453                         R.styleable.KeyboardLayoutSet_Element_supportsSplitLayout, false);
454                 elementParams.mAllowRedundantMoreKeys = a.getBoolean(
455                         R.styleable.KeyboardLayoutSet_Element_allowRedundantMoreKeys, true);
456                 mParams.mKeyboardLayoutSetElementIdToParamsMap.put(elementName, elementParams);
457             } finally {
458                 a.recycle();
459             }
460         }
461 
getKeyboardMode(final EditorInfo editorInfo)462         private static int getKeyboardMode(final EditorInfo editorInfo) {
463             final int inputType = editorInfo.inputType;
464             final int variation = inputType & InputType.TYPE_MASK_VARIATION;
465 
466             switch (inputType & InputType.TYPE_MASK_CLASS) {
467             case InputType.TYPE_CLASS_NUMBER:
468                 return KeyboardId.MODE_NUMBER;
469             case InputType.TYPE_CLASS_DATETIME:
470                 switch (variation) {
471                 case InputType.TYPE_DATETIME_VARIATION_DATE:
472                     return KeyboardId.MODE_DATE;
473                 case InputType.TYPE_DATETIME_VARIATION_TIME:
474                     return KeyboardId.MODE_TIME;
475                 default: // InputType.TYPE_DATETIME_VARIATION_NORMAL
476                     return KeyboardId.MODE_DATETIME;
477                 }
478             case InputType.TYPE_CLASS_PHONE:
479                 return KeyboardId.MODE_PHONE;
480             case InputType.TYPE_CLASS_TEXT:
481                 if (InputTypeUtils.isEmailVariation(variation)) {
482                     return KeyboardId.MODE_EMAIL;
483                 } else if (variation == InputType.TYPE_TEXT_VARIATION_URI) {
484                     return KeyboardId.MODE_URL;
485                 } else if (variation == InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE) {
486                     return KeyboardId.MODE_IM;
487                 } else if (variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
488                     return KeyboardId.MODE_TEXT;
489                 } else {
490                     return KeyboardId.MODE_TEXT;
491                 }
492             default:
493                 return KeyboardId.MODE_TEXT;
494             }
495         }
496     }
497 }
498