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.Constants.ImeOption.FORCE_ASCII;
20 import static com.android.inputmethod.latin.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.KeysCache;
38 import com.android.inputmethod.latin.InputAttributes;
39 import com.android.inputmethod.latin.R;
40 import com.android.inputmethod.latin.SubtypeSwitcher;
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 /**
55  * This class represents a set of keyboard layouts. Each of them represents a different keyboard
56  * specific to a keyboard state, such as alphabet, symbols, and so on.  Layouts in the same
57  * {@link KeyboardLayoutSet} are related to each other.
58  * A {@link KeyboardLayoutSet} needs to be created for each
59  * {@link android.view.inputmethod.EditorInfo}.
60  */
61 public final class KeyboardLayoutSet {
62     private static final String TAG = KeyboardLayoutSet.class.getSimpleName();
63     private static final boolean DEBUG_CACHE = DebugFlags.DEBUG_ENABLED;
64 
65     private static final String TAG_KEYBOARD_SET = "KeyboardLayoutSet";
66     private static final String TAG_ELEMENT = "Element";
67     private static final String TAG_FEATURE = "Feature";
68 
69     private static final String KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX = "keyboard_layout_set_";
70 
71     private final Context mContext;
72     private final Params mParams;
73 
74     // How many layouts we forcibly keep in cache. This only includes ALPHABET (default) and
75     // ALPHABET_AUTOMATIC_SHIFTED layouts - other layouts may stay in memory in the map of
76     // soft-references, but we forcibly cache this many alphabetic/auto-shifted layouts.
77     private static final int FORCIBLE_CACHE_SIZE = 4;
78     // By construction of soft references, anything that is also referenced somewhere else
79     // will stay in the cache. So we forcibly keep some references in an array to prevent
80     // them from disappearing from sKeyboardCache.
81     private static final Keyboard[] sForcibleKeyboardCache = new Keyboard[FORCIBLE_CACHE_SIZE];
82     private static final HashMap<KeyboardId, SoftReference<Keyboard>> sKeyboardCache =
83             new HashMap<>();
84     private static final KeysCache sKeysCache = new KeysCache();
85 
86     @SuppressWarnings("serial")
87     public static final class KeyboardLayoutSetException extends RuntimeException {
88         public final KeyboardId mKeyboardId;
89 
KeyboardLayoutSetException(final Throwable cause, final KeyboardId keyboardId)90         public KeyboardLayoutSetException(final Throwable cause, final KeyboardId keyboardId) {
91             super(cause);
92             mKeyboardId = keyboardId;
93         }
94     }
95 
96     private static final class ElementParams {
97         int mKeyboardXmlId;
98         boolean mProximityCharsCorrectionEnabled;
ElementParams()99         public ElementParams() {}
100     }
101 
102     public static final class Params {
103         String mKeyboardLayoutSetName;
104         int mMode;
105         boolean mDisableTouchPositionCorrectionDataForTest;
106         // TODO: Use {@link InputAttributes} instead of these variables.
107         EditorInfo mEditorInfo;
108         boolean mIsPasswordField;
109         boolean mVoiceInputKeyEnabled;
110         boolean mNoSettingsKey;
111         boolean mLanguageSwitchKeyEnabled;
112         InputMethodSubtype mSubtype;
113         boolean mIsSpellChecker;
114         int mKeyboardWidth;
115         int mKeyboardHeight;
116         int mScriptId = ScriptUtils.SCRIPT_LATIN;
117         // Sparse array of KeyboardLayoutSet element parameters indexed by element's id.
118         final SparseArray<ElementParams> mKeyboardLayoutSetElementIdToParamsMap =
119                 new SparseArray<>();
120     }
121 
onSystemLocaleChanged()122     public static void onSystemLocaleChanged() {
123         clearKeyboardCache();
124     }
125 
onKeyboardThemeChanged()126     public static void onKeyboardThemeChanged() {
127         clearKeyboardCache();
128     }
129 
clearKeyboardCache()130     private static void clearKeyboardCache() {
131         sKeyboardCache.clear();
132         sKeysCache.clear();
133     }
134 
KeyboardLayoutSet(final Context context, final Params params)135     KeyboardLayoutSet(final Context context, final Params params) {
136         mContext = context;
137         mParams = params;
138     }
139 
getKeyboard(final int baseKeyboardLayoutSetElementId)140     public Keyboard getKeyboard(final int baseKeyboardLayoutSetElementId) {
141         final int keyboardLayoutSetElementId;
142         switch (mParams.mMode) {
143         case KeyboardId.MODE_PHONE:
144             if (baseKeyboardLayoutSetElementId == KeyboardId.ELEMENT_SYMBOLS) {
145                 keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE_SYMBOLS;
146             } else {
147                 keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE;
148             }
149             break;
150         case KeyboardId.MODE_NUMBER:
151         case KeyboardId.MODE_DATE:
152         case KeyboardId.MODE_TIME:
153         case KeyboardId.MODE_DATETIME:
154             keyboardLayoutSetElementId = KeyboardId.ELEMENT_NUMBER;
155             break;
156         default:
157             keyboardLayoutSetElementId = baseKeyboardLayoutSetElementId;
158             break;
159         }
160 
161         ElementParams elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get(
162                 keyboardLayoutSetElementId);
163         if (elementParams == null) {
164             elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get(
165                     KeyboardId.ELEMENT_ALPHABET);
166         }
167         // Note: The keyboard for each shift state, and mode are represented as an elementName
168         // attribute in a keyboard_layout_set XML file.  Also each keyboard layout XML resource is
169         // specified as an elementKeyboard attribute in the file.
170         // The KeyboardId is an internal key for a Keyboard object.
171         final KeyboardId id = new KeyboardId(keyboardLayoutSetElementId, mParams);
172         try {
173             return getKeyboard(elementParams, id);
174         } catch (final RuntimeException e) {
175             Log.e(TAG, "Can't create keyboard: " + id, e);
176             throw new KeyboardLayoutSetException(e, id);
177         }
178     }
179 
getKeyboard(final ElementParams elementParams, final KeyboardId id)180     private Keyboard getKeyboard(final ElementParams elementParams, final KeyboardId id) {
181         final SoftReference<Keyboard> ref = sKeyboardCache.get(id);
182         final Keyboard cachedKeyboard = (ref == null) ? null : ref.get();
183         if (cachedKeyboard != null) {
184             if (DEBUG_CACHE) {
185                 Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": HIT  id=" + id);
186             }
187             return cachedKeyboard;
188         }
189 
190         final KeyboardBuilder<KeyboardParams> builder =
191                 new KeyboardBuilder<>(mContext, new KeyboardParams());
192         if (id.isAlphabetKeyboard()) {
193             builder.setAutoGenerate(sKeysCache);
194         }
195         final int keyboardXmlId = elementParams.mKeyboardXmlId;
196         builder.load(keyboardXmlId, id);
197         if (mParams.mDisableTouchPositionCorrectionDataForTest) {
198             builder.disableTouchPositionCorrectionDataForTest();
199         }
200         builder.setProximityCharsCorrectionEnabled(elementParams.mProximityCharsCorrectionEnabled);
201         final Keyboard keyboard = builder.build();
202         sKeyboardCache.put(id, new SoftReference<>(keyboard));
203         if ((id.mElementId == KeyboardId.ELEMENT_ALPHABET
204                 || id.mElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED)
205                 && !mParams.mIsSpellChecker) {
206             // We only forcibly cache the primary, "ALPHABET", layouts.
207             for (int i = sForcibleKeyboardCache.length - 1; i >= 1; --i) {
208                 sForcibleKeyboardCache[i] = sForcibleKeyboardCache[i - 1];
209             }
210             sForcibleKeyboardCache[0] = keyboard;
211             if (DEBUG_CACHE) {
212                 Log.d(TAG, "forcing caching of keyboard with id=" + id);
213             }
214         }
215         if (DEBUG_CACHE) {
216             Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": "
217                     + ((ref == null) ? "LOAD" : "GCed") + " id=" + id);
218         }
219         return keyboard;
220     }
221 
getScriptId()222     public int getScriptId() {
223         return mParams.mScriptId;
224     }
225 
226     public static final class Builder {
227         private final Context mContext;
228         private final String mPackageName;
229         private final Resources mResources;
230 
231         private final Params mParams = new Params();
232 
233         private static final EditorInfo EMPTY_EDITOR_INFO = new EditorInfo();
234 
Builder(final Context context, final EditorInfo ei)235         public Builder(final Context context, final EditorInfo ei) {
236             mContext = context;
237             mPackageName = context.getPackageName();
238             mResources = context.getResources();
239             final Params params = mParams;
240 
241             final EditorInfo editorInfo = (ei != null) ? ei : EMPTY_EDITOR_INFO;
242             params.mMode = getKeyboardMode(editorInfo);
243             // TODO: Consolidate those with {@link InputAttributes}.
244             params.mEditorInfo = editorInfo;
245             params.mIsPasswordField = InputTypeUtils.isPasswordInputType(editorInfo.inputType);
246             params.mNoSettingsKey = InputAttributes.inPrivateImeOptions(
247                     mPackageName, NO_SETTINGS_KEY, editorInfo);
248         }
249 
setKeyboardGeometry(final int keyboardWidth, final int keyboardHeight)250         public Builder setKeyboardGeometry(final int keyboardWidth, final int keyboardHeight) {
251             mParams.mKeyboardWidth = keyboardWidth;
252             mParams.mKeyboardHeight = keyboardHeight;
253             return this;
254         }
255 
setSubtype(final InputMethodSubtype subtype)256         public Builder setSubtype(final InputMethodSubtype subtype) {
257             final boolean asciiCapable = InputMethodSubtypeCompatUtils.isAsciiCapable(subtype);
258             // TODO: Consolidate with {@link InputAttributes}.
259             @SuppressWarnings("deprecation")
260             final boolean deprecatedForceAscii = InputAttributes.inPrivateImeOptions(
261                     mPackageName, FORCE_ASCII, mParams.mEditorInfo);
262             final boolean forceAscii = EditorInfoCompatUtils.hasFlagForceAscii(
263                     mParams.mEditorInfo.imeOptions)
264                     || deprecatedForceAscii;
265             final InputMethodSubtype keyboardSubtype = (forceAscii && !asciiCapable)
266                     ? SubtypeSwitcher.getInstance().getNoLanguageSubtype()
267                     : subtype;
268             mParams.mSubtype = keyboardSubtype;
269             mParams.mKeyboardLayoutSetName = KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX
270                     + SubtypeLocaleUtils.getKeyboardLayoutSetName(keyboardSubtype);
271             return this;
272         }
273 
setIsSpellChecker(final boolean isSpellChecker)274         public Builder setIsSpellChecker(final boolean isSpellChecker) {
275             mParams.mIsSpellChecker = isSpellChecker;
276             return this;
277         }
278 
setVoiceInputKeyEnabled(final boolean enabled)279         public Builder setVoiceInputKeyEnabled(final boolean enabled) {
280             mParams.mVoiceInputKeyEnabled = enabled;
281             return this;
282         }
283 
setLanguageSwitchKeyEnabled(final boolean enabled)284         public Builder setLanguageSwitchKeyEnabled(final boolean enabled) {
285             mParams.mLanguageSwitchKeyEnabled = enabled;
286             return this;
287         }
288 
disableTouchPositionCorrectionData()289         public void disableTouchPositionCorrectionData() {
290             mParams.mDisableTouchPositionCorrectionDataForTest = true;
291         }
292 
setScriptId(final int scriptId)293         public void setScriptId(final int scriptId) {
294             mParams.mScriptId = scriptId;
295         }
296 
build()297         public KeyboardLayoutSet build() {
298             if (mParams.mSubtype == null)
299                 throw new RuntimeException("KeyboardLayoutSet subtype is not specified");
300             final String packageName = mResources.getResourcePackageName(
301                     R.xml.keyboard_layout_set_qwerty);
302             final String keyboardLayoutSetName = mParams.mKeyboardLayoutSetName;
303             final int xmlId = mResources.getIdentifier(keyboardLayoutSetName, "xml", packageName);
304             try {
305                 parseKeyboardLayoutSet(mResources, xmlId);
306             } catch (final IOException e) {
307                 throw new RuntimeException(e.getMessage() + " in " + keyboardLayoutSetName, e);
308             } catch (final XmlPullParserException e) {
309                 throw new RuntimeException(e.getMessage() + " in " + keyboardLayoutSetName, e);
310             }
311             return new KeyboardLayoutSet(mContext, mParams);
312         }
313 
parseKeyboardLayoutSet(final Resources res, final int resId)314         private void parseKeyboardLayoutSet(final Resources res, final int resId)
315                 throws XmlPullParserException, IOException {
316             final XmlResourceParser parser = res.getXml(resId);
317             try {
318                 while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
319                     final int event = parser.next();
320                     if (event == XmlPullParser.START_TAG) {
321                         final String tag = parser.getName();
322                         if (TAG_KEYBOARD_SET.equals(tag)) {
323                             parseKeyboardLayoutSetContent(parser);
324                         } else {
325                             throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD_SET);
326                         }
327                     }
328                 }
329             } finally {
330                 parser.close();
331             }
332         }
333 
parseKeyboardLayoutSetContent(final XmlPullParser parser)334         private void parseKeyboardLayoutSetContent(final XmlPullParser parser)
335                 throws XmlPullParserException, IOException {
336             while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
337                 final int event = parser.next();
338                 if (event == XmlPullParser.START_TAG) {
339                     final String tag = parser.getName();
340                     if (TAG_ELEMENT.equals(tag)) {
341                         parseKeyboardLayoutSetElement(parser);
342                     } else if (TAG_FEATURE.equals(tag)) {
343                         parseKeyboardLayoutSetFeature(parser);
344                     } else {
345                         throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD_SET);
346                     }
347                 } else if (event == XmlPullParser.END_TAG) {
348                     final String tag = parser.getName();
349                     if (TAG_KEYBOARD_SET.equals(tag)) {
350                         break;
351                     } else {
352                         throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_KEYBOARD_SET);
353                     }
354                 }
355             }
356         }
357 
parseKeyboardLayoutSetElement(final XmlPullParser parser)358         private void parseKeyboardLayoutSetElement(final XmlPullParser parser)
359                 throws XmlPullParserException, IOException {
360             final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
361                     R.styleable.KeyboardLayoutSet_Element);
362             try {
363                 XmlParseUtils.checkAttributeExists(a,
364                         R.styleable.KeyboardLayoutSet_Element_elementName, "elementName",
365                         TAG_ELEMENT, parser);
366                 XmlParseUtils.checkAttributeExists(a,
367                         R.styleable.KeyboardLayoutSet_Element_elementKeyboard, "elementKeyboard",
368                         TAG_ELEMENT, parser);
369                 XmlParseUtils.checkEndTag(TAG_ELEMENT, parser);
370 
371                 final ElementParams elementParams = new ElementParams();
372                 final int elementName = a.getInt(
373                         R.styleable.KeyboardLayoutSet_Element_elementName, 0);
374                 elementParams.mKeyboardXmlId = a.getResourceId(
375                         R.styleable.KeyboardLayoutSet_Element_elementKeyboard, 0);
376                 elementParams.mProximityCharsCorrectionEnabled = a.getBoolean(
377                         R.styleable.KeyboardLayoutSet_Element_enableProximityCharsCorrection,
378                         false);
379                 mParams.mKeyboardLayoutSetElementIdToParamsMap.put(elementName, elementParams);
380             } finally {
381                 a.recycle();
382             }
383         }
384 
parseKeyboardLayoutSetFeature(final XmlPullParser parser)385         private void parseKeyboardLayoutSetFeature(final XmlPullParser parser)
386                 throws XmlPullParserException, IOException {
387             final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
388                     R.styleable.KeyboardLayoutSet_Feature);
389             try {
390                 final int scriptId = a.getInt(
391                         R.styleable.KeyboardLayoutSet_Feature_supportedScript,
392                         ScriptUtils.SCRIPT_LATIN);
393                 XmlParseUtils.checkEndTag(TAG_FEATURE, parser);
394                 setScriptId(scriptId);
395             } finally {
396                 a.recycle();
397             }
398         }
399 
getKeyboardMode(final EditorInfo editorInfo)400         private static int getKeyboardMode(final EditorInfo editorInfo) {
401             final int inputType = editorInfo.inputType;
402             final int variation = inputType & InputType.TYPE_MASK_VARIATION;
403 
404             switch (inputType & InputType.TYPE_MASK_CLASS) {
405             case InputType.TYPE_CLASS_NUMBER:
406                 return KeyboardId.MODE_NUMBER;
407             case InputType.TYPE_CLASS_DATETIME:
408                 switch (variation) {
409                 case InputType.TYPE_DATETIME_VARIATION_DATE:
410                     return KeyboardId.MODE_DATE;
411                 case InputType.TYPE_DATETIME_VARIATION_TIME:
412                     return KeyboardId.MODE_TIME;
413                 default: // InputType.TYPE_DATETIME_VARIATION_NORMAL
414                     return KeyboardId.MODE_DATETIME;
415                 }
416             case InputType.TYPE_CLASS_PHONE:
417                 return KeyboardId.MODE_PHONE;
418             case InputType.TYPE_CLASS_TEXT:
419                 if (InputTypeUtils.isEmailVariation(variation)) {
420                     return KeyboardId.MODE_EMAIL;
421                 } else if (variation == InputType.TYPE_TEXT_VARIATION_URI) {
422                     return KeyboardId.MODE_URL;
423                 } else if (variation == InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE) {
424                     return KeyboardId.MODE_IM;
425                 } else if (variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
426                     return KeyboardId.MODE_TEXT;
427                 } else {
428                     return KeyboardId.MODE_TEXT;
429                 }
430             default:
431                 return KeyboardId.MODE_TEXT;
432             }
433         }
434     }
435 }
436