KeyboardLayoutSet.java revision ecfbf4625c8afd9cde7b79e0c7846b87e20f79e9
1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * 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, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * 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.util.Log;
32import android.util.SparseArray;
33import android.util.Xml;
34import android.view.inputmethod.EditorInfo;
35import android.view.inputmethod.InputMethodSubtype;
36
37import com.android.inputmethod.compat.EditorInfoCompatUtils;
38import com.android.inputmethod.keyboard.KeyboardLayoutSet.Params.ElementParams;
39import com.android.inputmethod.latin.CollectionUtils;
40import com.android.inputmethod.latin.InputAttributes;
41import com.android.inputmethod.latin.InputTypeUtils;
42import com.android.inputmethod.latin.LatinImeLogger;
43import com.android.inputmethod.latin.R;
44import com.android.inputmethod.latin.SubtypeLocale;
45import com.android.inputmethod.latin.SubtypeSwitcher;
46import com.android.inputmethod.latin.XmlParseUtils;
47
48import org.xmlpull.v1.XmlPullParser;
49import org.xmlpull.v1.XmlPullParserException;
50
51import java.io.IOException;
52import java.lang.ref.SoftReference;
53import java.util.HashMap;
54
55/**
56 * This class represents a set of keyboard layouts. Each of them represents a different keyboard
57 * specific to a keyboard state, such as alphabet, symbols, and so on.  Layouts in the same
58 * {@link KeyboardLayoutSet} are related to each other.
59 * A {@link KeyboardLayoutSet} needs to be created for each
60 * {@link android.view.inputmethod.EditorInfo}.
61 */
62public class KeyboardLayoutSet {
63    private static final String TAG = KeyboardLayoutSet.class.getSimpleName();
64    private static final boolean DEBUG_CACHE = LatinImeLogger.sDBG;
65
66    private static final String TAG_KEYBOARD_SET = "KeyboardLayoutSet";
67    private static final String TAG_ELEMENT = "Element";
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    private static final HashMap<KeyboardId, SoftReference<Keyboard>> sKeyboardCache =
75            CollectionUtils.newHashMap();
76    private static final KeysCache sKeysCache = new KeysCache();
77
78    public static class KeyboardLayoutSetException extends RuntimeException {
79        public final KeyboardId mKeyboardId;
80
81        public KeyboardLayoutSetException(Throwable cause, KeyboardId keyboardId) {
82            super(cause);
83            mKeyboardId = keyboardId;
84        }
85    }
86
87    public static class KeysCache {
88        private final HashMap<Key, Key> mMap = CollectionUtils.newHashMap();
89
90        public void clear() {
91            mMap.clear();
92        }
93
94        public Key get(Key key) {
95            final Key existingKey = mMap.get(key);
96            if (existingKey != null) {
97                // Reuse the existing element that equals to "key" without adding "key" to the map.
98                return existingKey;
99            }
100            mMap.put(key, key);
101            return key;
102        }
103    }
104
105    static class Params {
106        String mKeyboardLayoutSetName;
107        int mMode;
108        EditorInfo mEditorInfo;
109        boolean mTouchPositionCorrectionEnabled;
110        boolean mVoiceKeyEnabled;
111        boolean mVoiceKeyOnMain;
112        boolean mNoSettingsKey;
113        boolean mLanguageSwitchKeyEnabled;
114        InputMethodSubtype mSubtype;
115        int mDeviceFormFactor;
116        int mOrientation;
117        int mWidth;
118        // Sparse array of KeyboardLayoutSet element parameters indexed by element's id.
119        final SparseArray<ElementParams> mKeyboardLayoutSetElementIdToParamsMap =
120                CollectionUtils.newSparseArray();
121
122        static class ElementParams {
123            int mKeyboardXmlId;
124            boolean mProximityCharsCorrectionEnabled;
125        }
126    }
127
128    public static void clearKeyboardCache() {
129        sKeyboardCache.clear();
130        sKeysCache.clear();
131    }
132
133    private KeyboardLayoutSet(Context context, Params params) {
134        mContext = context;
135        mParams = params;
136    }
137
138    public Keyboard getKeyboard(int baseKeyboardLayoutSetElementId) {
139        final int keyboardLayoutSetElementId;
140        switch (mParams.mMode) {
141        case KeyboardId.MODE_PHONE:
142            if (baseKeyboardLayoutSetElementId == KeyboardId.ELEMENT_SYMBOLS) {
143                keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE_SYMBOLS;
144            } else {
145                keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE;
146            }
147            break;
148        case KeyboardId.MODE_NUMBER:
149        case KeyboardId.MODE_DATE:
150        case KeyboardId.MODE_TIME:
151        case KeyboardId.MODE_DATETIME:
152            keyboardLayoutSetElementId = KeyboardId.ELEMENT_NUMBER;
153            break;
154        default:
155            keyboardLayoutSetElementId = baseKeyboardLayoutSetElementId;
156            break;
157        }
158
159        ElementParams elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get(
160                keyboardLayoutSetElementId);
161        if (elementParams == null) {
162            elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get(
163                    KeyboardId.ELEMENT_ALPHABET);
164        }
165        final KeyboardId id = getKeyboardId(keyboardLayoutSetElementId);
166        try {
167            return getKeyboard(elementParams, id);
168        } catch (RuntimeException e) {
169            throw new KeyboardLayoutSetException(e, id);
170        }
171    }
172
173    private Keyboard getKeyboard(ElementParams elementParams, final KeyboardId id) {
174        final SoftReference<Keyboard> ref = sKeyboardCache.get(id);
175        Keyboard keyboard = (ref == null) ? null : ref.get();
176        if (keyboard == null) {
177            final Keyboard.Builder<Keyboard.Params> builder =
178                    new Keyboard.Builder<Keyboard.Params>(mContext, new Keyboard.Params());
179            if (id.isAlphabetKeyboard()) {
180                builder.setAutoGenerate(sKeysCache);
181            }
182            final int keyboardXmlId = elementParams.mKeyboardXmlId;
183            builder.load(keyboardXmlId, id);
184            builder.setTouchPositionCorrectionEnabled(mParams.mTouchPositionCorrectionEnabled);
185            builder.setProximityCharsCorrectionEnabled(
186                    elementParams.mProximityCharsCorrectionEnabled);
187            keyboard = builder.build();
188            sKeyboardCache.put(id, new SoftReference<Keyboard>(keyboard));
189
190            if (DEBUG_CACHE) {
191                Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": "
192                        + ((ref == null) ? "LOAD" : "GCed") + " id=" + id);
193            }
194        } else if (DEBUG_CACHE) {
195            Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": HIT  id=" + id);
196        }
197
198        return keyboard;
199    }
200
201    // Note: The keyboard for each locale, shift state, and mode are represented as
202    // KeyboardLayoutSet element id that is a key in keyboard_set.xml.  Also that file specifies
203    // which XML layout should be used for each keyboard.  The KeyboardId is an internal key for
204    // Keyboard object.
205    private KeyboardId getKeyboardId(int keyboardLayoutSetElementId) {
206        final Params params = mParams;
207        final boolean isSymbols = (keyboardLayoutSetElementId == KeyboardId.ELEMENT_SYMBOLS
208                || keyboardLayoutSetElementId == KeyboardId.ELEMENT_SYMBOLS_SHIFTED);
209        final boolean noLanguage = SubtypeLocale.isNoLanguage(params.mSubtype);
210        final boolean voiceKeyEnabled = params.mVoiceKeyEnabled && !noLanguage;
211        final boolean hasShortcutKey = voiceKeyEnabled && (isSymbols != params.mVoiceKeyOnMain);
212        return new KeyboardId(keyboardLayoutSetElementId, params.mSubtype, params.mDeviceFormFactor,
213                params.mOrientation, params.mWidth, params.mMode, params.mEditorInfo,
214                params.mNoSettingsKey, voiceKeyEnabled, hasShortcutKey,
215                params.mLanguageSwitchKeyEnabled);
216    }
217
218    public static class Builder {
219        private final Context mContext;
220        private final String mPackageName;
221        private final Resources mResources;
222        private final EditorInfo mEditorInfo;
223
224        private final Params mParams = new Params();
225
226        private static final EditorInfo EMPTY_EDITOR_INFO = new EditorInfo();
227
228        public Builder(Context context, EditorInfo editorInfo) {
229            mContext = context;
230            mPackageName = context.getPackageName();
231            mResources = context.getResources();
232            mEditorInfo = editorInfo;
233            final Params params = mParams;
234
235            params.mMode = getKeyboardMode(editorInfo);
236            params.mEditorInfo = (editorInfo != null) ? editorInfo : EMPTY_EDITOR_INFO;
237            params.mNoSettingsKey = InputAttributes.inPrivateImeOptions(
238                    mPackageName, NO_SETTINGS_KEY, mEditorInfo);
239        }
240
241        public Builder setScreenGeometry(int deviceFormFactor, int orientation, int widthPixels) {
242            final Params params = mParams;
243            params.mDeviceFormFactor = deviceFormFactor;
244            params.mOrientation = orientation;
245            params.mWidth = widthPixels;
246            return this;
247        }
248
249        public Builder setSubtype(InputMethodSubtype subtype) {
250            final boolean asciiCapable = subtype.containsExtraValueKey(ASCII_CAPABLE);
251            @SuppressWarnings("deprecation")
252            final boolean deprecatedForceAscii = InputAttributes.inPrivateImeOptions(
253                    mPackageName, FORCE_ASCII, mEditorInfo);
254            final boolean forceAscii = EditorInfoCompatUtils.hasFlagForceAscii(
255                    mParams.mEditorInfo.imeOptions)
256                    || deprecatedForceAscii;
257            final InputMethodSubtype keyboardSubtype = (forceAscii && !asciiCapable)
258                    ? SubtypeSwitcher.getInstance().getNoLanguageSubtype()
259                    : subtype;
260            mParams.mSubtype = keyboardSubtype;
261            mParams.mKeyboardLayoutSetName = KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX
262                    + SubtypeLocale.getKeyboardLayoutSetName(keyboardSubtype);
263            return this;
264        }
265
266        public Builder setOptions(boolean voiceKeyEnabled, boolean voiceKeyOnMain,
267                boolean languageSwitchKeyEnabled) {
268            @SuppressWarnings("deprecation")
269            final boolean deprecatedNoMicrophone = InputAttributes.inPrivateImeOptions(
270                    null, NO_MICROPHONE_COMPAT, mEditorInfo);
271            final boolean noMicrophone = InputAttributes.inPrivateImeOptions(
272                    mPackageName, NO_MICROPHONE, mEditorInfo)
273                    || deprecatedNoMicrophone;
274            mParams.mVoiceKeyEnabled = voiceKeyEnabled && !noMicrophone;
275            mParams.mVoiceKeyOnMain = voiceKeyOnMain;
276            mParams.mLanguageSwitchKeyEnabled = languageSwitchKeyEnabled;
277            return this;
278        }
279
280        public void setTouchPositionCorrectionEnabled(boolean enabled) {
281            mParams.mTouchPositionCorrectionEnabled = enabled;
282        }
283
284        public KeyboardLayoutSet build() {
285            if (mParams.mOrientation == Configuration.ORIENTATION_UNDEFINED)
286                throw new RuntimeException("Screen geometry is not specified");
287            if (mParams.mSubtype == null)
288                throw new RuntimeException("KeyboardLayoutSet subtype is not specified");
289            final String packageName = mResources.getResourcePackageName(
290                    R.xml.keyboard_layout_set_qwerty);
291            final String keyboardLayoutSetName = mParams.mKeyboardLayoutSetName;
292            final int xmlId = mResources.getIdentifier(keyboardLayoutSetName, "xml", packageName);
293            try {
294                parseKeyboardLayoutSet(mResources, xmlId);
295            } catch (Exception e) {
296                throw new RuntimeException(e.getMessage() + " in " + keyboardLayoutSetName);
297            }
298            return new KeyboardLayoutSet(mContext, mParams);
299        }
300
301        private void parseKeyboardLayoutSet(Resources res, int resId)
302                throws XmlPullParserException, IOException {
303            final XmlResourceParser parser = res.getXml(resId);
304            try {
305                int event;
306                while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
307                    if (event == XmlPullParser.START_TAG) {
308                        final String tag = parser.getName();
309                        if (TAG_KEYBOARD_SET.equals(tag)) {
310                            parseKeyboardLayoutSetContent(parser);
311                        } else {
312                            throw new XmlParseUtils.IllegalStartTag(parser, TAG_KEYBOARD_SET);
313                        }
314                    }
315                }
316            } finally {
317                parser.close();
318            }
319        }
320
321        private void parseKeyboardLayoutSetContent(XmlPullParser parser)
322                throws XmlPullParserException, IOException {
323            int event;
324            while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
325                if (event == XmlPullParser.START_TAG) {
326                    final String tag = parser.getName();
327                    if (TAG_ELEMENT.equals(tag)) {
328                        parseKeyboardLayoutSetElement(parser);
329                    } else {
330                        throw new XmlParseUtils.IllegalStartTag(parser, TAG_KEYBOARD_SET);
331                    }
332                } else if (event == XmlPullParser.END_TAG) {
333                    final String tag = parser.getName();
334                    if (TAG_KEYBOARD_SET.equals(tag)) {
335                        break;
336                    } else {
337                        throw new XmlParseUtils.IllegalEndTag(parser, TAG_KEYBOARD_SET);
338                    }
339                }
340            }
341        }
342
343        private void parseKeyboardLayoutSetElement(XmlPullParser parser)
344                throws XmlPullParserException, IOException {
345            final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
346                    R.styleable.KeyboardLayoutSet_Element);
347            try {
348                XmlParseUtils.checkAttributeExists(a,
349                        R.styleable.KeyboardLayoutSet_Element_elementName, "elementName",
350                        TAG_ELEMENT, parser);
351                XmlParseUtils.checkAttributeExists(a,
352                        R.styleable.KeyboardLayoutSet_Element_elementKeyboard, "elementKeyboard",
353                        TAG_ELEMENT, parser);
354                XmlParseUtils.checkEndTag(TAG_ELEMENT, parser);
355
356                final ElementParams elementParams = new ElementParams();
357                final int elementName = a.getInt(
358                        R.styleable.KeyboardLayoutSet_Element_elementName, 0);
359                elementParams.mKeyboardXmlId = a.getResourceId(
360                        R.styleable.KeyboardLayoutSet_Element_elementKeyboard, 0);
361                elementParams.mProximityCharsCorrectionEnabled = a.getBoolean(
362                        R.styleable.KeyboardLayoutSet_Element_enableProximityCharsCorrection,
363                        false);
364                mParams.mKeyboardLayoutSetElementIdToParamsMap.put(elementName, elementParams);
365            } finally {
366                a.recycle();
367            }
368        }
369
370        private static int getKeyboardMode(EditorInfo editorInfo) {
371            if (editorInfo == null)
372                return KeyboardId.MODE_TEXT;
373
374            final int inputType = editorInfo.inputType;
375            final int variation = inputType & InputType.TYPE_MASK_VARIATION;
376
377            switch (inputType & InputType.TYPE_MASK_CLASS) {
378            case InputType.TYPE_CLASS_NUMBER:
379                return KeyboardId.MODE_NUMBER;
380            case InputType.TYPE_CLASS_DATETIME:
381                switch (variation) {
382                case InputType.TYPE_DATETIME_VARIATION_DATE:
383                    return KeyboardId.MODE_DATE;
384                case InputType.TYPE_DATETIME_VARIATION_TIME:
385                    return KeyboardId.MODE_TIME;
386                default: // InputType.TYPE_DATETIME_VARIATION_NORMAL
387                    return KeyboardId.MODE_DATETIME;
388                }
389            case InputType.TYPE_CLASS_PHONE:
390                return KeyboardId.MODE_PHONE;
391            case InputType.TYPE_CLASS_TEXT:
392                if (InputTypeUtils.isEmailVariation(variation)) {
393                    return KeyboardId.MODE_EMAIL;
394                } else if (variation == InputType.TYPE_TEXT_VARIATION_URI) {
395                    return KeyboardId.MODE_URL;
396                } else if (variation == InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE) {
397                    return KeyboardId.MODE_IM;
398                } else if (variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
399                    return KeyboardId.MODE_TEXT;
400                } else {
401                    return KeyboardId.MODE_TEXT;
402                }
403            default:
404                return KeyboardId.MODE_TEXT;
405            }
406        }
407    }
408}
409