KeyboardLayoutSet.java revision aca2ef85e1af82ccadbd0cbdd691a680a03a824d
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.Constants.ImeOption.FORCE_ASCII;
20import static com.android.inputmethod.latin.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.LatinImeLogger;
40import com.android.inputmethod.latin.R;
41import com.android.inputmethod.latin.SubtypeSwitcher;
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 = LatinImeLogger.sDBG;
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        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;
117        // Sparse array of KeyboardLayoutSet element parameters indexed by element's id.
118        final SparseArray<ElementParams> mKeyboardLayoutSetElementIdToParamsMap =
119                new SparseArray<>();
120    }
121
122    public static void clearKeyboardCache() {
123        sKeyboardCache.clear();
124        sKeysCache.clear();
125    }
126
127    KeyboardLayoutSet(final Context context, final Params params) {
128        mContext = context;
129        mParams = params;
130    }
131
132    public Keyboard getKeyboard(final int baseKeyboardLayoutSetElementId) {
133        final int keyboardLayoutSetElementId;
134        switch (mParams.mMode) {
135        case KeyboardId.MODE_PHONE:
136            if (baseKeyboardLayoutSetElementId == KeyboardId.ELEMENT_SYMBOLS) {
137                keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE_SYMBOLS;
138            } else {
139                keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE;
140            }
141            break;
142        case KeyboardId.MODE_NUMBER:
143        case KeyboardId.MODE_DATE:
144        case KeyboardId.MODE_TIME:
145        case KeyboardId.MODE_DATETIME:
146            keyboardLayoutSetElementId = KeyboardId.ELEMENT_NUMBER;
147            break;
148        default:
149            keyboardLayoutSetElementId = baseKeyboardLayoutSetElementId;
150            break;
151        }
152
153        ElementParams elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get(
154                keyboardLayoutSetElementId);
155        if (elementParams == null) {
156            elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get(
157                    KeyboardId.ELEMENT_ALPHABET);
158        }
159        // Note: The keyboard for each shift state, and mode are represented as an elementName
160        // attribute in a keyboard_layout_set XML file.  Also each keyboard layout XML resource is
161        // specified as an elementKeyboard attribute in the file.
162        // The KeyboardId is an internal key for a Keyboard object.
163        final KeyboardId id = new KeyboardId(keyboardLayoutSetElementId, mParams);
164        try {
165            return getKeyboard(elementParams, id);
166        } catch (final RuntimeException e) {
167            Log.e(TAG, "Can't create keyboard: " + id, e);
168            throw new KeyboardLayoutSetException(e, id);
169        }
170    }
171
172    private Keyboard getKeyboard(final ElementParams elementParams, final KeyboardId id) {
173        final SoftReference<Keyboard> ref = sKeyboardCache.get(id);
174        final Keyboard cachedKeyboard = (ref == null) ? null : ref.get();
175        if (cachedKeyboard != null) {
176            if (DEBUG_CACHE) {
177                Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": HIT  id=" + id);
178            }
179            return cachedKeyboard;
180        }
181
182        final KeyboardBuilder<KeyboardParams> builder =
183                new KeyboardBuilder<>(mContext, new KeyboardParams());
184        if (id.isAlphabetKeyboard()) {
185            builder.setAutoGenerate(sKeysCache);
186        }
187        final int keyboardXmlId = elementParams.mKeyboardXmlId;
188        builder.load(keyboardXmlId, id);
189        if (mParams.mDisableTouchPositionCorrectionDataForTest) {
190            builder.disableTouchPositionCorrectionDataForTest();
191        }
192        builder.setProximityCharsCorrectionEnabled(elementParams.mProximityCharsCorrectionEnabled);
193        final Keyboard keyboard = builder.build();
194        sKeyboardCache.put(id, new SoftReference<>(keyboard));
195        if ((id.mElementId == KeyboardId.ELEMENT_ALPHABET
196                || id.mElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED)
197                && !mParams.mIsSpellChecker) {
198            // We only forcibly cache the primary, "ALPHABET", layouts.
199            for (int i = sForcibleKeyboardCache.length - 1; i >= 1; --i) {
200                sForcibleKeyboardCache[i] = sForcibleKeyboardCache[i - 1];
201            }
202            sForcibleKeyboardCache[0] = keyboard;
203            if (DEBUG_CACHE) {
204                Log.d(TAG, "forcing caching of keyboard with id=" + id);
205            }
206        }
207        if (DEBUG_CACHE) {
208            Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": "
209                    + ((ref == null) ? "LOAD" : "GCed") + " id=" + id);
210        }
211        return keyboard;
212    }
213
214    public static final class Builder {
215        private final Context mContext;
216        private final String mPackageName;
217        private final Resources mResources;
218
219        private final Params mParams = new Params();
220
221        private static final EditorInfo EMPTY_EDITOR_INFO = new EditorInfo();
222
223        public Builder(final Context context, final EditorInfo ei) {
224            mContext = context;
225            mPackageName = context.getPackageName();
226            mResources = context.getResources();
227            final Params params = mParams;
228
229            final EditorInfo editorInfo = (ei != null) ? ei : EMPTY_EDITOR_INFO;
230            params.mMode = getKeyboardMode(editorInfo);
231            // TODO: Consolidate those with {@link InputAttributes}.
232            params.mEditorInfo = editorInfo;
233            params.mIsPasswordField = InputTypeUtils.isPasswordInputType(editorInfo.inputType);
234            params.mNoSettingsKey = InputAttributes.inPrivateImeOptions(
235                    mPackageName, NO_SETTINGS_KEY, editorInfo);
236        }
237
238        public Builder setKeyboardGeometry(final int keyboardWidth, final int keyboardHeight) {
239            mParams.mKeyboardWidth = keyboardWidth;
240            mParams.mKeyboardHeight = keyboardHeight;
241            return this;
242        }
243
244        public Builder setSubtype(final InputMethodSubtype subtype) {
245            final boolean asciiCapable = InputMethodSubtypeCompatUtils.isAsciiCapable(subtype);
246            // TODO: Consolidate with {@link InputAttributes}.
247            @SuppressWarnings("deprecation")
248            final boolean deprecatedForceAscii = InputAttributes.inPrivateImeOptions(
249                    mPackageName, FORCE_ASCII, mParams.mEditorInfo);
250            final boolean forceAscii = EditorInfoCompatUtils.hasFlagForceAscii(
251                    mParams.mEditorInfo.imeOptions)
252                    || deprecatedForceAscii;
253            final InputMethodSubtype keyboardSubtype = (forceAscii && !asciiCapable)
254                    ? SubtypeSwitcher.getInstance().getNoLanguageSubtype()
255                    : subtype;
256            mParams.mSubtype = keyboardSubtype;
257            mParams.mKeyboardLayoutSetName = KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX
258                    + SubtypeLocaleUtils.getKeyboardLayoutSetName(keyboardSubtype);
259            return this;
260        }
261
262        public Builder setIsSpellChecker(final boolean isSpellChecker) {
263            mParams.mIsSpellChecker = isSpellChecker;
264            return this;
265        }
266
267        public Builder setVoiceInputKeyEnabled(final boolean enabled) {
268            mParams.mVoiceInputKeyEnabled = enabled;
269            return this;
270        }
271
272        public Builder setLanguageSwitchKeyEnabled(final boolean enabled) {
273            mParams.mLanguageSwitchKeyEnabled = enabled;
274            return this;
275        }
276
277        public void disableTouchPositionCorrectionData() {
278            mParams.mDisableTouchPositionCorrectionDataForTest = true;
279        }
280
281        public void setScriptId(final int scriptId) {
282            mParams.mScriptId = scriptId;
283        }
284
285        public KeyboardLayoutSet build() {
286            if (mParams.mSubtype == null)
287                throw new RuntimeException("KeyboardLayoutSet subtype is not specified");
288            final String packageName = mResources.getResourcePackageName(
289                    R.xml.keyboard_layout_set_qwerty);
290            final String keyboardLayoutSetName = mParams.mKeyboardLayoutSetName;
291            final int xmlId = mResources.getIdentifier(keyboardLayoutSetName, "xml", packageName);
292            try {
293                parseKeyboardLayoutSet(mResources, xmlId);
294            } catch (final IOException e) {
295                throw new RuntimeException(e.getMessage() + " in " + keyboardLayoutSetName, e);
296            } catch (final XmlPullParserException e) {
297                throw new RuntimeException(e.getMessage() + " in " + keyboardLayoutSetName, e);
298            }
299            return new KeyboardLayoutSet(mContext, mParams);
300        }
301
302        private void parseKeyboardLayoutSet(final Resources res, final int resId)
303                throws XmlPullParserException, IOException {
304            final XmlResourceParser parser = res.getXml(resId);
305            try {
306                while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
307                    final int event = parser.next();
308                    if (event == XmlPullParser.START_TAG) {
309                        final String tag = parser.getName();
310                        if (TAG_KEYBOARD_SET.equals(tag)) {
311                            parseKeyboardLayoutSetContent(parser);
312                        } else {
313                            throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD_SET);
314                        }
315                    }
316                }
317            } finally {
318                parser.close();
319            }
320        }
321
322        private void parseKeyboardLayoutSetContent(final XmlPullParser parser)
323                throws XmlPullParserException, IOException {
324            while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
325                final int event = parser.next();
326                if (event == XmlPullParser.START_TAG) {
327                    final String tag = parser.getName();
328                    if (TAG_ELEMENT.equals(tag)) {
329                        parseKeyboardLayoutSetElement(parser);
330                    } else if (TAG_FEATURE.equals(tag)) {
331                        parseKeyboardLayoutSetFeature(parser);
332                    } else {
333                        throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD_SET);
334                    }
335                } else if (event == XmlPullParser.END_TAG) {
336                    final String tag = parser.getName();
337                    if (TAG_KEYBOARD_SET.equals(tag)) {
338                        break;
339                    } else {
340                        throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_KEYBOARD_SET);
341                    }
342                }
343            }
344        }
345
346        private void parseKeyboardLayoutSetElement(final XmlPullParser parser)
347                throws XmlPullParserException, IOException {
348            final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
349                    R.styleable.KeyboardLayoutSet_Element);
350            try {
351                XmlParseUtils.checkAttributeExists(a,
352                        R.styleable.KeyboardLayoutSet_Element_elementName, "elementName",
353                        TAG_ELEMENT, parser);
354                XmlParseUtils.checkAttributeExists(a,
355                        R.styleable.KeyboardLayoutSet_Element_elementKeyboard, "elementKeyboard",
356                        TAG_ELEMENT, parser);
357                XmlParseUtils.checkEndTag(TAG_ELEMENT, parser);
358
359                final ElementParams elementParams = new ElementParams();
360                final int elementName = a.getInt(
361                        R.styleable.KeyboardLayoutSet_Element_elementName, 0);
362                elementParams.mKeyboardXmlId = a.getResourceId(
363                        R.styleable.KeyboardLayoutSet_Element_elementKeyboard, 0);
364                elementParams.mProximityCharsCorrectionEnabled = a.getBoolean(
365                        R.styleable.KeyboardLayoutSet_Element_enableProximityCharsCorrection,
366                        false);
367                mParams.mKeyboardLayoutSetElementIdToParamsMap.put(elementName, elementParams);
368            } finally {
369                a.recycle();
370            }
371        }
372
373        private void parseKeyboardLayoutSetFeature(final XmlPullParser parser)
374                throws XmlPullParserException, IOException {
375            final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
376                    R.styleable.KeyboardLayoutSet_Feature);
377            try {
378                final int scriptId = a.getInt(
379                        R.styleable.KeyboardLayoutSet_Feature_supportedScript,
380                        ScriptUtils.SCRIPT_LATIN);
381                XmlParseUtils.checkEndTag(TAG_FEATURE, parser);
382                setScriptId(scriptId);
383            } finally {
384                a.recycle();
385            }
386        }
387
388        private static int getKeyboardMode(final EditorInfo editorInfo) {
389            final int inputType = editorInfo.inputType;
390            final int variation = inputType & InputType.TYPE_MASK_VARIATION;
391
392            switch (inputType & InputType.TYPE_MASK_CLASS) {
393            case InputType.TYPE_CLASS_NUMBER:
394                return KeyboardId.MODE_NUMBER;
395            case InputType.TYPE_CLASS_DATETIME:
396                switch (variation) {
397                case InputType.TYPE_DATETIME_VARIATION_DATE:
398                    return KeyboardId.MODE_DATE;
399                case InputType.TYPE_DATETIME_VARIATION_TIME:
400                    return KeyboardId.MODE_TIME;
401                default: // InputType.TYPE_DATETIME_VARIATION_NORMAL
402                    return KeyboardId.MODE_DATETIME;
403                }
404            case InputType.TYPE_CLASS_PHONE:
405                return KeyboardId.MODE_PHONE;
406            case InputType.TYPE_CLASS_TEXT:
407                if (InputTypeUtils.isEmailVariation(variation)) {
408                    return KeyboardId.MODE_EMAIL;
409                } else if (variation == InputType.TYPE_TEXT_VARIATION_URI) {
410                    return KeyboardId.MODE_URL;
411                } else if (variation == InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE) {
412                    return KeyboardId.MODE_IM;
413                } else if (variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
414                    return KeyboardId.MODE_TEXT;
415                } else {
416                    return KeyboardId.MODE_TEXT;
417                }
418            default:
419                return KeyboardId.MODE_TEXT;
420            }
421        }
422    }
423}
424