KeyboardLayoutSet.java revision f2eadbb497a172f602e49226a7f891d9e9097760
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.R;
40import com.android.inputmethod.latin.SubtypeSwitcher;
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        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
122    public static void onSystemLocaleChanged() {
123        clearKeyboardCache();
124    }
125
126    public static void onKeyboardThemeChanged() {
127        clearKeyboardCache();
128    }
129
130    private static void clearKeyboardCache() {
131        sKeyboardCache.clear();
132        sKeysCache.clear();
133    }
134
135    KeyboardLayoutSet(final Context context, final Params params) {
136        mContext = context;
137        mParams = params;
138    }
139
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
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
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
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
250        public Builder setKeyboardGeometry(final int keyboardWidth, final int keyboardHeight) {
251            mParams.mKeyboardWidth = keyboardWidth;
252            mParams.mKeyboardHeight = keyboardHeight;
253            return this;
254        }
255
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
274        public Builder setIsSpellChecker(final boolean isSpellChecker) {
275            mParams.mIsSpellChecker = isSpellChecker;
276            return this;
277        }
278
279        public Builder setVoiceInputKeyEnabled(final boolean enabled) {
280            mParams.mVoiceInputKeyEnabled = enabled;
281            return this;
282        }
283
284        public Builder setLanguageSwitchKeyEnabled(final boolean enabled) {
285            mParams.mLanguageSwitchKeyEnabled = enabled;
286            return this;
287        }
288
289        public void disableTouchPositionCorrectionData() {
290            mParams.mDisableTouchPositionCorrectionDataForTest = true;
291        }
292
293        public void setScriptId(final int scriptId) {
294            mParams.mScriptId = scriptId;
295        }
296
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
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
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
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
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
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