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