KeyboardLayoutSet.java revision 2a7da0ab87db1166c62c171858b589da3d9c2ca7
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
17package com.android.inputmethod.keyboard;
18
19import static com.android.inputmethod.latin.common.Constants.ImeOption.FORCE_ASCII;
20import static com.android.inputmethod.latin.common.Constants.ImeOption.NO_SETTINGS_KEY;
21
22import android.content.Context;
23import android.content.res.Resources;
24import android.content.res.TypedArray;
25import android.content.res.XmlResourceParser;
26import android.text.InputType;
27import android.util.Log;
28import android.util.SparseArray;
29import android.util.Xml;
30import android.view.inputmethod.EditorInfo;
31import android.view.inputmethod.InputMethodSubtype;
32
33import com.android.inputmethod.compat.EditorInfoCompatUtils;
34import com.android.inputmethod.compat.InputMethodSubtypeCompatUtils;
35import com.android.inputmethod.keyboard.internal.KeyboardBuilder;
36import com.android.inputmethod.keyboard.internal.KeyboardParams;
37import com.android.inputmethod.keyboard.internal.KeysCache;
38import com.android.inputmethod.latin.InputAttributes;
39import com.android.inputmethod.latin.R;
40import com.android.inputmethod.latin.RichInputMethodSubtype;
41import com.android.inputmethod.latin.define.DebugFlags;
42import com.android.inputmethod.latin.utils.InputTypeUtils;
43import com.android.inputmethod.latin.utils.ScriptUtils;
44import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
45import com.android.inputmethod.latin.utils.XmlParseUtils;
46
47import org.xmlpull.v1.XmlPullParser;
48import org.xmlpull.v1.XmlPullParserException;
49
50import java.io.IOException;
51import java.lang.ref.SoftReference;
52import 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 */
61public 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
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;
99        boolean mSupportsSplitLayout;
100        boolean mAllowRedundantMoreKeys;
101        public ElementParams() {}
102    }
103
104    public static final class Params {
105        String mKeyboardLayoutSetName;
106        int mMode;
107        boolean mDisableTouchPositionCorrectionDataForTest;
108        // TODO: Use {@link InputAttributes} instead of these variables.
109        EditorInfo mEditorInfo;
110        boolean mIsPasswordField;
111        boolean mVoiceInputKeyEnabled;
112        boolean mNoSettingsKey;
113        boolean mLanguageSwitchKeyEnabled;
114        RichInputMethodSubtype mSubtype;
115        boolean mIsSpellChecker;
116        int mKeyboardWidth;
117        int mKeyboardHeight;
118        int mScriptId = ScriptUtils.SCRIPT_LATIN;
119        // Indicates if the user has enabled the split-layout preference
120        // and the required ProductionFlags are enabled.
121        boolean mIsSplitLayoutEnabledByUser;
122        // Indicates if split layout is actually enabled, taking into account
123        // whether the user has enabled it, and the keyboard layout supports it.
124        boolean mIsSplitLayoutEnabled;
125        // Sparse array of KeyboardLayoutSet element parameters indexed by element's id.
126        final SparseArray<ElementParams> mKeyboardLayoutSetElementIdToParamsMap =
127                new SparseArray<>();
128    }
129
130    public static void onSystemLocaleChanged() {
131        clearKeyboardCache();
132    }
133
134    public static void onKeyboardThemeChanged() {
135        clearKeyboardCache();
136    }
137
138    private static void clearKeyboardCache() {
139        sKeyboardCache.clear();
140        sKeysCache.clear();
141    }
142
143    KeyboardLayoutSet(final Context context, final Params params) {
144        mContext = context;
145        mParams = params;
146    }
147
148    public Keyboard getKeyboard(final int baseKeyboardLayoutSetElementId) {
149        final int keyboardLayoutSetElementId;
150        switch (mParams.mMode) {
151        case KeyboardId.MODE_PHONE:
152            if (baseKeyboardLayoutSetElementId == KeyboardId.ELEMENT_SYMBOLS) {
153                keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE_SYMBOLS;
154            } else {
155                keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE;
156            }
157            break;
158        case KeyboardId.MODE_NUMBER:
159        case KeyboardId.MODE_DATE:
160        case KeyboardId.MODE_TIME:
161        case KeyboardId.MODE_DATETIME:
162            keyboardLayoutSetElementId = KeyboardId.ELEMENT_NUMBER;
163            break;
164        default:
165            keyboardLayoutSetElementId = baseKeyboardLayoutSetElementId;
166            break;
167        }
168
169        ElementParams elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get(
170                keyboardLayoutSetElementId);
171        if (elementParams == null) {
172            elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get(
173                    KeyboardId.ELEMENT_ALPHABET);
174        }
175        // Note: The keyboard for each shift state, and mode are represented as an elementName
176        // attribute in a keyboard_layout_set XML file.  Also each keyboard layout XML resource is
177        // specified as an elementKeyboard attribute in the file.
178        // The KeyboardId is an internal key for a Keyboard object.
179
180        mParams.mIsSplitLayoutEnabled = mParams.mIsSplitLayoutEnabledByUser
181                && elementParams.mSupportsSplitLayout;
182        final KeyboardId id = new KeyboardId(keyboardLayoutSetElementId, mParams);
183        try {
184            return getKeyboard(elementParams, id);
185        } catch (final RuntimeException e) {
186            Log.e(TAG, "Can't create keyboard: " + id, e);
187            throw new KeyboardLayoutSetException(e, id);
188        }
189    }
190
191    private Keyboard getKeyboard(final ElementParams elementParams, final KeyboardId id) {
192        final SoftReference<Keyboard> ref = sKeyboardCache.get(id);
193        final Keyboard cachedKeyboard = (ref == null) ? null : ref.get();
194        if (cachedKeyboard != null) {
195            if (DEBUG_CACHE) {
196                Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": HIT  id=" + id);
197            }
198            return cachedKeyboard;
199        }
200
201        final KeyboardBuilder<KeyboardParams> builder =
202                new KeyboardBuilder<>(mContext, new KeyboardParams());
203        if (id.isAlphabetKeyboard()) {
204            builder.setAutoGenerate(sKeysCache);
205        }
206        builder.setAllowRedundantMoreKes(elementParams.mAllowRedundantMoreKeys);
207        final int keyboardXmlId = elementParams.mKeyboardXmlId;
208        builder.load(keyboardXmlId, id);
209        if (mParams.mDisableTouchPositionCorrectionDataForTest) {
210            builder.disableTouchPositionCorrectionDataForTest();
211        }
212        builder.setProximityCharsCorrectionEnabled(elementParams.mProximityCharsCorrectionEnabled);
213        final Keyboard keyboard = builder.build();
214        sKeyboardCache.put(id, new SoftReference<>(keyboard));
215        if ((id.mElementId == KeyboardId.ELEMENT_ALPHABET
216                || id.mElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED)
217                && !mParams.mIsSpellChecker) {
218            // We only forcibly cache the primary, "ALPHABET", layouts.
219            for (int i = sForcibleKeyboardCache.length - 1; i >= 1; --i) {
220                sForcibleKeyboardCache[i] = sForcibleKeyboardCache[i - 1];
221            }
222            sForcibleKeyboardCache[0] = keyboard;
223            if (DEBUG_CACHE) {
224                Log.d(TAG, "forcing caching of keyboard with id=" + id);
225            }
226        }
227        if (DEBUG_CACHE) {
228            Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": "
229                    + ((ref == null) ? "LOAD" : "GCed") + " id=" + id);
230        }
231        return keyboard;
232    }
233
234    public int getScriptId() {
235        return mParams.mScriptId;
236    }
237
238    public static final class Builder {
239        private final Context mContext;
240        private final String mPackageName;
241        private final Resources mResources;
242
243        private final Params mParams = new Params();
244
245        private static final EditorInfo EMPTY_EDITOR_INFO = new EditorInfo();
246
247        public Builder(final Context context, final EditorInfo ei) {
248            mContext = context;
249            mPackageName = context.getPackageName();
250            mResources = context.getResources();
251            final Params params = mParams;
252
253            final EditorInfo editorInfo = (ei != null) ? ei : EMPTY_EDITOR_INFO;
254            params.mMode = getKeyboardMode(editorInfo);
255            // TODO: Consolidate those with {@link InputAttributes}.
256            params.mEditorInfo = editorInfo;
257            params.mIsPasswordField = InputTypeUtils.isPasswordInputType(editorInfo.inputType);
258            params.mNoSettingsKey = InputAttributes.inPrivateImeOptions(
259                    mPackageName, NO_SETTINGS_KEY, editorInfo);
260        }
261
262        public Builder setKeyboardGeometry(final int keyboardWidth, final int keyboardHeight) {
263            mParams.mKeyboardWidth = keyboardWidth;
264            mParams.mKeyboardHeight = keyboardHeight;
265            return this;
266        }
267
268        public Builder setSubtype(final RichInputMethodSubtype subtype) {
269            final boolean asciiCapable = InputMethodSubtypeCompatUtils.isAsciiCapable(subtype);
270            // TODO: Consolidate with {@link InputAttributes}.
271            @SuppressWarnings("deprecation")
272            final boolean deprecatedForceAscii = InputAttributes.inPrivateImeOptions(
273                    mPackageName, FORCE_ASCII, mParams.mEditorInfo);
274            final boolean forceAscii = EditorInfoCompatUtils.hasFlagForceAscii(
275                    mParams.mEditorInfo.imeOptions)
276                    || deprecatedForceAscii;
277            final RichInputMethodSubtype keyboardSubtype = (forceAscii && !asciiCapable)
278                    ? RichInputMethodSubtype.getNoLanguageSubtype()
279                    : subtype;
280            mParams.mSubtype = keyboardSubtype;
281            mParams.mKeyboardLayoutSetName = KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX
282                    + SubtypeLocaleUtils.getKeyboardLayoutSetName(keyboardSubtype);
283            return this;
284        }
285
286        public Builder setIsSpellChecker(final boolean isSpellChecker) {
287            mParams.mIsSpellChecker = isSpellChecker;
288            return this;
289        }
290
291        public Builder setVoiceInputKeyEnabled(final boolean enabled) {
292            mParams.mVoiceInputKeyEnabled = enabled;
293            return this;
294        }
295
296        public Builder setLanguageSwitchKeyEnabled(final boolean enabled) {
297            mParams.mLanguageSwitchKeyEnabled = enabled;
298            return this;
299        }
300
301        public Builder disableTouchPositionCorrectionData() {
302            mParams.mDisableTouchPositionCorrectionDataForTest = true;
303            return this;
304        }
305
306        public Builder setScriptId(final int scriptId) {
307            mParams.mScriptId = scriptId;
308            return this;
309        }
310
311        public Builder setSplitLayoutEnabledByUser(final boolean enabled) {
312            mParams.mIsSplitLayoutEnabledByUser = enabled;
313            return this;
314        }
315
316        private final static HashMap<InputMethodSubtype, Integer> sScriptIdsForSubtypes =
317                new HashMap<>();
318        public static int getScriptId(final Resources resources, final InputMethodSubtype subtype) {
319            final Integer value = sScriptIdsForSubtypes.get(subtype);
320            if (null == value) {
321                final int scriptId = readScriptId(resources, subtype);
322                sScriptIdsForSubtypes.put(subtype, scriptId);
323                return scriptId;
324            }
325            return value;
326        }
327
328        // Super redux version of reading the script ID for some subtype from Xml.
329        private static int readScriptId(final Resources resources,
330                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
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
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
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
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
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                        parseKeyboardLayoutSetFeature(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
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
462        private void parseKeyboardLayoutSetFeature(final XmlPullParser parser)
463                throws XmlPullParserException, IOException {
464            final int scriptId = readScriptIdFromTagFeature(mResources, parser);
465            setScriptId(scriptId);
466        }
467
468        private static int getKeyboardMode(final EditorInfo editorInfo) {
469            final int inputType = editorInfo.inputType;
470            final int variation = inputType & InputType.TYPE_MASK_VARIATION;
471
472            switch (inputType & InputType.TYPE_MASK_CLASS) {
473            case InputType.TYPE_CLASS_NUMBER:
474                return KeyboardId.MODE_NUMBER;
475            case InputType.TYPE_CLASS_DATETIME:
476                switch (variation) {
477                case InputType.TYPE_DATETIME_VARIATION_DATE:
478                    return KeyboardId.MODE_DATE;
479                case InputType.TYPE_DATETIME_VARIATION_TIME:
480                    return KeyboardId.MODE_TIME;
481                default: // InputType.TYPE_DATETIME_VARIATION_NORMAL
482                    return KeyboardId.MODE_DATETIME;
483                }
484            case InputType.TYPE_CLASS_PHONE:
485                return KeyboardId.MODE_PHONE;
486            case InputType.TYPE_CLASS_TEXT:
487                if (InputTypeUtils.isEmailVariation(variation)) {
488                    return KeyboardId.MODE_EMAIL;
489                } else if (variation == InputType.TYPE_TEXT_VARIATION_URI) {
490                    return KeyboardId.MODE_URL;
491                } else if (variation == InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE) {
492                    return KeyboardId.MODE_IM;
493                } else if (variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
494                    return KeyboardId.MODE_TEXT;
495                } else {
496                    return KeyboardId.MODE_TEXT;
497                }
498            default:
499                return KeyboardId.MODE_TEXT;
500            }
501        }
502    }
503}
504