• 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.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.Resources;
27 import android.content.res.TypedArray;
28 import android.content.res.XmlResourceParser;
29 import android.text.InputType;
30 import android.util.Log;
31 import android.util.SparseArray;
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.internal.KeyboardBuilder;
38 import com.android.inputmethod.keyboard.internal.KeyboardParams;
39 import com.android.inputmethod.keyboard.internal.KeysCache;
40 import com.android.inputmethod.latin.InputAttributes;
41 import com.android.inputmethod.latin.LatinImeLogger;
42 import com.android.inputmethod.latin.R;
43 import com.android.inputmethod.latin.SubtypeSwitcher;
44 import com.android.inputmethod.latin.utils.CollectionUtils;
45 import com.android.inputmethod.latin.utils.InputTypeUtils;
46 import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
47 import com.android.inputmethod.latin.utils.XmlParseUtils;
48 
49 import org.xmlpull.v1.XmlPullParser;
50 import org.xmlpull.v1.XmlPullParserException;
51 
52 import java.io.IOException;
53 import java.lang.ref.SoftReference;
54 import 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  */
63 public 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 
KeyboardLayoutSetException(final Throwable cause, final KeyboardId keyboardId)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;
ElementParams()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 
clearKeyboardCache()123     public static void clearKeyboardCache() {
124         sKeyboardCache.clear();
125         sKeysCache.clear();
126     }
127 
KeyboardLayoutSet(final Context context, final Params params)128     KeyboardLayoutSet(final Context context, final Params params) {
129         mContext = context;
130         mParams = params;
131     }
132 
getKeyboard(final int baseKeyboardLayoutSetElementId)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 
getKeyboard(final ElementParams elementParams, final KeyboardId id)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 
Builder(final Context context, final EditorInfo editorInfo)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 
setKeyboardGeometry(final int keyboardWidth, final int keyboardHeight)236         public Builder setKeyboardGeometry(final int keyboardWidth, final int keyboardHeight) {
237             mParams.mKeyboardWidth = keyboardWidth;
238             mParams.mKeyboardHeight = keyboardHeight;
239             return this;
240         }
241 
setSubtype(final InputMethodSubtype subtype)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 
setIsSpellChecker(final boolean isSpellChecker)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
setOptions(final boolean voiceKeyEnabled, final boolean voiceKeyOnMain, final boolean languageSwitchKeyEnabled)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 
disableTouchPositionCorrectionData()280         public void disableTouchPositionCorrectionData() {
281             mParams.mDisableTouchPositionCorrectionDataForTest = true;
282         }
283 
build()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 
parseKeyboardLayoutSet(final Resources res, final int resId)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 
parseKeyboardLayoutSetContent(final XmlPullParser parser)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 
parseKeyboardLayoutSetElement(final XmlPullParser parser)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 
getKeyboardMode(final EditorInfo editorInfo)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