KeyboardLayoutSet.java revision a410cb48eab0cd75aa27e20f60e47a29a59fb9ff
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.Configuration;
27import android.content.res.Resources;
28import android.content.res.TypedArray;
29import android.content.res.XmlResourceParser;
30import android.text.InputType;
31import android.text.TextUtils;
32import android.util.DisplayMetrics;
33import android.util.Log;
34import android.util.SparseArray;
35import android.util.Xml;
36import android.view.inputmethod.EditorInfo;
37import android.view.inputmethod.InputMethodSubtype;
38
39import com.android.inputmethod.annotations.UsedForTesting;
40import com.android.inputmethod.compat.EditorInfoCompatUtils;
41import com.android.inputmethod.keyboard.internal.KeyboardBuilder;
42import com.android.inputmethod.keyboard.internal.KeyboardParams;
43import com.android.inputmethod.keyboard.internal.KeysCache;
44import com.android.inputmethod.latin.InputAttributes;
45import com.android.inputmethod.latin.LatinImeLogger;
46import com.android.inputmethod.latin.R;
47import com.android.inputmethod.latin.SubtypeSwitcher;
48import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils;
49import com.android.inputmethod.latin.utils.CollectionUtils;
50import com.android.inputmethod.latin.utils.InputTypeUtils;
51import com.android.inputmethod.latin.utils.ResourceUtils;
52import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
53import com.android.inputmethod.latin.utils.XmlParseUtils;
54
55import org.xmlpull.v1.XmlPullParser;
56import org.xmlpull.v1.XmlPullParserException;
57
58import java.io.IOException;
59import java.lang.ref.SoftReference;
60import java.util.HashMap;
61
62/**
63 * This class represents a set of keyboard layouts. Each of them represents a different keyboard
64 * specific to a keyboard state, such as alphabet, symbols, and so on.  Layouts in the same
65 * {@link KeyboardLayoutSet} are related to each other.
66 * A {@link KeyboardLayoutSet} needs to be created for each
67 * {@link android.view.inputmethod.EditorInfo}.
68 */
69public final class KeyboardLayoutSet {
70    private static final String TAG = KeyboardLayoutSet.class.getSimpleName();
71    private static final boolean DEBUG_CACHE = LatinImeLogger.sDBG;
72
73    private static final String TAG_KEYBOARD_SET = "KeyboardLayoutSet";
74    private static final String TAG_ELEMENT = "Element";
75
76    private static final String KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX = "keyboard_layout_set_";
77    private static final int SPELLCHECKER_DUMMY_KEYBOARD_WIDTH = 480;
78    private static final int SPELLCHECKER_DUMMY_KEYBOARD_HEIGHT = 800;
79
80    private final Context mContext;
81    private final Params mParams;
82
83    // How many layouts we forcibly keep in cache. This only includes ALPHABET (default) and
84    // ALPHABET_AUTOMATIC_SHIFTED layouts - other layouts may stay in memory in the map of
85    // soft-references, but we forcibly cache this many alphabetic/auto-shifted layouts.
86    private static final int FORCIBLE_CACHE_SIZE = 4;
87    // By construction of soft references, anything that is also referenced somewhere else
88    // will stay in the cache. So we forcibly keep some references in an array to prevent
89    // them from disappearing from sKeyboardCache.
90    private static final Keyboard[] sForcibleKeyboardCache = new Keyboard[FORCIBLE_CACHE_SIZE];
91    private static final HashMap<KeyboardId, SoftReference<Keyboard>> sKeyboardCache =
92            CollectionUtils.newHashMap();
93    private static final KeysCache sKeysCache = new KeysCache();
94
95    @SuppressWarnings("serial")
96    public static final class KeyboardLayoutSetException extends RuntimeException {
97        public final KeyboardId mKeyboardId;
98
99        public KeyboardLayoutSetException(final Throwable cause, final KeyboardId keyboardId) {
100            super(cause);
101            mKeyboardId = keyboardId;
102        }
103    }
104
105    private static final class ElementParams {
106        int mKeyboardXmlId;
107        boolean mProximityCharsCorrectionEnabled;
108        public ElementParams() {}
109    }
110
111    public static final class Params {
112        String mKeyboardLayoutSetName;
113        int mMode;
114        EditorInfo mEditorInfo;
115        boolean mDisableTouchPositionCorrectionDataForTest;
116        boolean mVoiceKeyEnabled;
117        boolean mVoiceKeyOnMain;
118        boolean mNoSettingsKey;
119        boolean mLanguageSwitchKeyEnabled;
120        InputMethodSubtype mSubtype;
121        boolean mIsSpellChecker;
122        int mOrientation;
123        int mKeyboardWidth;
124        int mKeyboardHeight;
125        // Sparse array of KeyboardLayoutSet element parameters indexed by element's id.
126        final SparseArray<ElementParams> mKeyboardLayoutSetElementIdToParamsMap =
127                CollectionUtils.newSparseArray();
128    }
129
130    public 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 (RuntimeException e) {
175            throw new KeyboardLayoutSetException(e, id);
176        }
177    }
178
179    private Keyboard getKeyboard(final ElementParams elementParams, final KeyboardId id) {
180        final SoftReference<Keyboard> ref = sKeyboardCache.get(id);
181        Keyboard keyboard = (ref == null) ? null : ref.get();
182        if (keyboard == null) {
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(
194                    elementParams.mProximityCharsCorrectionEnabled);
195            keyboard = builder.build();
196            sKeyboardCache.put(id, new SoftReference<Keyboard>(keyboard));
197            if ((id.mElementId == KeyboardId.ELEMENT_ALPHABET
198                    || id.mElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED)
199                    && !mParams.mIsSpellChecker) {
200                // We only forcibly cache the primary, "ALPHABET", layouts.
201                for (int i = sForcibleKeyboardCache.length - 1; i >= 1; --i) {
202                    sForcibleKeyboardCache[i] = sForcibleKeyboardCache[i - 1];
203                }
204                sForcibleKeyboardCache[0] = keyboard;
205                if (DEBUG_CACHE) {
206                    Log.d(TAG, "forcing caching of keyboard with id=" + id);
207                }
208            }
209            if (DEBUG_CACHE) {
210                Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": "
211                        + ((ref == null) ? "LOAD" : "GCed") + " id=" + id);
212            }
213        } else if (DEBUG_CACHE) {
214            Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": HIT  id=" + id);
215        }
216
217        return keyboard;
218    }
219
220    public static final class Builder {
221        private final Context mContext;
222        private final String mPackageName;
223        private final Resources mResources;
224        private final EditorInfo mEditorInfo;
225
226        private final Params mParams = new Params();
227
228        private static final EditorInfo EMPTY_EDITOR_INFO = new EditorInfo();
229
230        public Builder(final Context context, final EditorInfo editorInfo) {
231            mContext = context;
232            mPackageName = context.getPackageName();
233            mResources = context.getResources();
234            mEditorInfo = editorInfo;
235            final Params params = mParams;
236
237            params.mMode = getKeyboardMode(editorInfo);
238            params.mEditorInfo = (editorInfo != null) ? editorInfo : EMPTY_EDITOR_INFO;
239            params.mNoSettingsKey = InputAttributes.inPrivateImeOptions(
240                    mPackageName, NO_SETTINGS_KEY, mEditorInfo);
241        }
242
243        public Builder setScreenGeometry(final int widthPixels, final int heightPixels) {
244            final Params params = mParams;
245            params.mOrientation = (heightPixels > widthPixels)
246                    ? Configuration.ORIENTATION_PORTRAIT : Configuration.ORIENTATION_LANDSCAPE;
247            setDefaultKeyboardSize(widthPixels, heightPixels);
248            return this;
249        }
250
251        private void setDefaultKeyboardSize(final int widthPixels, final int heightPixels) {
252            final String keyboardHeightString = ResourceUtils.getDeviceOverrideValue(
253                    mResources, R.array.keyboard_heights);
254            final float keyboardHeight;
255            if (TextUtils.isEmpty(keyboardHeightString)) {
256                keyboardHeight = mResources.getDimension(R.dimen.keyboardHeight);
257            } else {
258                keyboardHeight = Float.parseFloat(keyboardHeightString)
259                        * mResources.getDisplayMetrics().density;
260            }
261            final float maxKeyboardHeight = mResources.getFraction(
262                    R.fraction.maxKeyboardHeight, heightPixels, heightPixels);
263            float minKeyboardHeight = mResources.getFraction(
264                    R.fraction.minKeyboardHeight, heightPixels, heightPixels);
265            if (minKeyboardHeight < 0.0f) {
266                // Specified fraction was negative, so it should be calculated against display
267                // width.
268                minKeyboardHeight = -mResources.getFraction(
269                        R.fraction.minKeyboardHeight, widthPixels, widthPixels);
270            }
271            // Keyboard height will not exceed maxKeyboardHeight and will not be less than
272            // minKeyboardHeight.
273            mParams.mKeyboardHeight = (int)Math.max(
274                    Math.min(keyboardHeight, maxKeyboardHeight), minKeyboardHeight);
275            mParams.mKeyboardWidth = widthPixels;
276        }
277
278        public Builder setSubtype(final InputMethodSubtype subtype) {
279            final boolean asciiCapable = subtype.containsExtraValueKey(ASCII_CAPABLE);
280            @SuppressWarnings("deprecation")
281            final boolean deprecatedForceAscii = InputAttributes.inPrivateImeOptions(
282                    mPackageName, FORCE_ASCII, mEditorInfo);
283            final boolean forceAscii = EditorInfoCompatUtils.hasFlagForceAscii(
284                    mParams.mEditorInfo.imeOptions)
285                    || deprecatedForceAscii;
286            final InputMethodSubtype keyboardSubtype = (forceAscii && !asciiCapable)
287                    ? SubtypeSwitcher.getInstance().getNoLanguageSubtype()
288                    : subtype;
289            mParams.mSubtype = keyboardSubtype;
290            mParams.mKeyboardLayoutSetName = KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX
291                    + SubtypeLocaleUtils.getKeyboardLayoutSetName(keyboardSubtype);
292            return this;
293        }
294
295        public Builder setIsSpellChecker(final boolean isSpellChecker) {
296            mParams.mIsSpellChecker = true;
297            return this;
298        }
299
300        public Builder setOptions(final boolean voiceKeyEnabled, final boolean voiceKeyOnMain,
301                final boolean languageSwitchKeyEnabled) {
302            @SuppressWarnings("deprecation")
303            final boolean deprecatedNoMicrophone = InputAttributes.inPrivateImeOptions(
304                    null, NO_MICROPHONE_COMPAT, mEditorInfo);
305            final boolean noMicrophone = InputAttributes.inPrivateImeOptions(
306                    mPackageName, NO_MICROPHONE, mEditorInfo)
307                    || deprecatedNoMicrophone;
308            mParams.mVoiceKeyEnabled = voiceKeyEnabled && !noMicrophone;
309            mParams.mVoiceKeyOnMain = voiceKeyOnMain;
310            mParams.mLanguageSwitchKeyEnabled = languageSwitchKeyEnabled;
311            return this;
312        }
313
314        public void disableTouchPositionCorrectionData() {
315            mParams.mDisableTouchPositionCorrectionDataForTest = true;
316        }
317
318        public KeyboardLayoutSet build() {
319            if (mParams.mOrientation == Configuration.ORIENTATION_UNDEFINED)
320                throw new RuntimeException("Screen geometry is not specified");
321            if (mParams.mSubtype == null)
322                throw new RuntimeException("KeyboardLayoutSet subtype is not specified");
323            final String packageName = mResources.getResourcePackageName(
324                    R.xml.keyboard_layout_set_qwerty);
325            final String keyboardLayoutSetName = mParams.mKeyboardLayoutSetName;
326            final int xmlId = mResources.getIdentifier(keyboardLayoutSetName, "xml", packageName);
327            try {
328                parseKeyboardLayoutSet(mResources, xmlId);
329            } catch (final IOException e) {
330                throw new RuntimeException(e.getMessage() + " in " + keyboardLayoutSetName, e);
331            } catch (final XmlPullParserException e) {
332                throw new RuntimeException(e.getMessage() + " in " + keyboardLayoutSetName, e);
333            }
334            return new KeyboardLayoutSet(mContext, mParams);
335        }
336
337        private void parseKeyboardLayoutSet(final Resources res, final int resId)
338                throws XmlPullParserException, IOException {
339            final XmlResourceParser parser = res.getXml(resId);
340            try {
341                while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
342                    final int event = parser.next();
343                    if (event == XmlPullParser.START_TAG) {
344                        final String tag = parser.getName();
345                        if (TAG_KEYBOARD_SET.equals(tag)) {
346                            parseKeyboardLayoutSetContent(parser);
347                        } else {
348                            throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD_SET);
349                        }
350                    }
351                }
352            } finally {
353                parser.close();
354            }
355        }
356
357        private void parseKeyboardLayoutSetContent(final XmlPullParser parser)
358                throws XmlPullParserException, IOException {
359            while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
360                final int event = parser.next();
361                if (event == XmlPullParser.START_TAG) {
362                    final String tag = parser.getName();
363                    if (TAG_ELEMENT.equals(tag)) {
364                        parseKeyboardLayoutSetElement(parser);
365                    } else {
366                        throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD_SET);
367                    }
368                } else if (event == XmlPullParser.END_TAG) {
369                    final String tag = parser.getName();
370                    if (TAG_KEYBOARD_SET.equals(tag)) {
371                        break;
372                    } else {
373                        throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_KEYBOARD_SET);
374                    }
375                }
376            }
377        }
378
379        private void parseKeyboardLayoutSetElement(final XmlPullParser parser)
380                throws XmlPullParserException, IOException {
381            final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
382                    R.styleable.KeyboardLayoutSet_Element);
383            try {
384                XmlParseUtils.checkAttributeExists(a,
385                        R.styleable.KeyboardLayoutSet_Element_elementName, "elementName",
386                        TAG_ELEMENT, parser);
387                XmlParseUtils.checkAttributeExists(a,
388                        R.styleable.KeyboardLayoutSet_Element_elementKeyboard, "elementKeyboard",
389                        TAG_ELEMENT, parser);
390                XmlParseUtils.checkEndTag(TAG_ELEMENT, parser);
391
392                final ElementParams elementParams = new ElementParams();
393                final int elementName = a.getInt(
394                        R.styleable.KeyboardLayoutSet_Element_elementName, 0);
395                elementParams.mKeyboardXmlId = a.getResourceId(
396                        R.styleable.KeyboardLayoutSet_Element_elementKeyboard, 0);
397                elementParams.mProximityCharsCorrectionEnabled = a.getBoolean(
398                        R.styleable.KeyboardLayoutSet_Element_enableProximityCharsCorrection,
399                        false);
400                mParams.mKeyboardLayoutSetElementIdToParamsMap.put(elementName, elementParams);
401            } finally {
402                a.recycle();
403            }
404        }
405
406        private static int getKeyboardMode(final EditorInfo editorInfo) {
407            if (editorInfo == null)
408                return KeyboardId.MODE_TEXT;
409
410            final int inputType = editorInfo.inputType;
411            final int variation = inputType & InputType.TYPE_MASK_VARIATION;
412
413            switch (inputType & InputType.TYPE_MASK_CLASS) {
414            case InputType.TYPE_CLASS_NUMBER:
415                return KeyboardId.MODE_NUMBER;
416            case InputType.TYPE_CLASS_DATETIME:
417                switch (variation) {
418                case InputType.TYPE_DATETIME_VARIATION_DATE:
419                    return KeyboardId.MODE_DATE;
420                case InputType.TYPE_DATETIME_VARIATION_TIME:
421                    return KeyboardId.MODE_TIME;
422                default: // InputType.TYPE_DATETIME_VARIATION_NORMAL
423                    return KeyboardId.MODE_DATETIME;
424                }
425            case InputType.TYPE_CLASS_PHONE:
426                return KeyboardId.MODE_PHONE;
427            case InputType.TYPE_CLASS_TEXT:
428                if (InputTypeUtils.isEmailVariation(variation)) {
429                    return KeyboardId.MODE_EMAIL;
430                } else if (variation == InputType.TYPE_TEXT_VARIATION_URI) {
431                    return KeyboardId.MODE_URL;
432                } else if (variation == InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE) {
433                    return KeyboardId.MODE_IM;
434                } else if (variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
435                    return KeyboardId.MODE_TEXT;
436                } else {
437                    return KeyboardId.MODE_TEXT;
438                }
439            default:
440                return KeyboardId.MODE_TEXT;
441            }
442        }
443    }
444
445    public static KeyboardLayoutSet createKeyboardSetForSpellChecker(final Context context,
446            final String locale, final String layout) {
447        final InputMethodSubtype subtype =
448                AdditionalSubtypeUtils.createAdditionalSubtype(locale, layout, null);
449        return createKeyboardSet(context, subtype, SPELLCHECKER_DUMMY_KEYBOARD_WIDTH,
450                SPELLCHECKER_DUMMY_KEYBOARD_HEIGHT, false /* testCasesHaveTouchCoordinates */,
451                true /* isSpellChecker */);
452    }
453
454    @UsedForTesting
455    public static KeyboardLayoutSet createKeyboardSetForTest(final Context context,
456            final InputMethodSubtype subtype, final int orientation,
457            final boolean testCasesHaveTouchCoordinates) {
458        final DisplayMetrics dm = context.getResources().getDisplayMetrics();
459        final int width;
460        final int height;
461        if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
462            width = Math.max(dm.widthPixels, dm.heightPixels);
463            height = Math.min(dm.widthPixels, dm.heightPixels);
464        } else if (orientation == Configuration.ORIENTATION_PORTRAIT) {
465            width = Math.min(dm.widthPixels, dm.heightPixels);
466            height = Math.max(dm.widthPixels, dm.heightPixels);
467        } else {
468            throw new RuntimeException("Orientation should be ORIENTATION_LANDSCAPE or "
469                    + "ORIENTATION_PORTRAIT: orientation=" + orientation);
470        }
471        return createKeyboardSet(context, subtype, width, height, testCasesHaveTouchCoordinates,
472                false /* isSpellChecker */);
473    }
474
475    private static KeyboardLayoutSet createKeyboardSet(final Context context,
476            final InputMethodSubtype subtype, final int width, final int height,
477            final boolean testCasesHaveTouchCoordinates, final boolean isSpellChecker) {
478        final EditorInfo editorInfo = new EditorInfo();
479        editorInfo.inputType = InputType.TYPE_CLASS_TEXT;
480        final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder(
481                context, editorInfo);
482        builder.setScreenGeometry(width, height);
483        builder.setSubtype(subtype);
484        builder.setIsSpellChecker(isSpellChecker);
485        if (!testCasesHaveTouchCoordinates) {
486            // For spell checker and tests
487            builder.disableTouchPositionCorrectionData();
488        }
489        return builder.build();
490    }
491}
492