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        // TODO: Remove mVoiceKeyOnMain when it's certainly confirmed that we no longer show
110        // the voice input key on the symbol layout
111        boolean mVoiceKeyOnMain;
112        boolean mNoSettingsKey;
113        boolean mLanguageSwitchKeyEnabled;
114        InputMethodSubtype mSubtype;
115        boolean mIsSpellChecker;
116        int mKeyboardWidth;
117        int mKeyboardHeight;
118        // Sparse array of KeyboardLayoutSet element parameters indexed by element's id.
119        final SparseArray<ElementParams> mKeyboardLayoutSetElementIdToParamsMap =
120                CollectionUtils.newSparseArray();
121    }
122
123    public static void clearKeyboardCache() {
124        sKeyboardCache.clear();
125        sKeysCache.clear();
126    }
127
128    KeyboardLayoutSet(final Context context, final Params params) {
129        mContext = context;
130        mParams = params;
131    }
132
133    public Keyboard getKeyboard(final int baseKeyboardLayoutSetElementId) {
134        final int keyboardLayoutSetElementId;
135        switch (mParams.mMode) {
136        case KeyboardId.MODE_PHONE:
137            if (baseKeyboardLayoutSetElementId == KeyboardId.ELEMENT_SYMBOLS) {
138                keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE_SYMBOLS;
139            } else {
140                keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE;
141            }
142            break;
143        case KeyboardId.MODE_NUMBER:
144        case KeyboardId.MODE_DATE:
145        case KeyboardId.MODE_TIME:
146        case KeyboardId.MODE_DATETIME:
147            keyboardLayoutSetElementId = KeyboardId.ELEMENT_NUMBER;
148            break;
149        default:
150            keyboardLayoutSetElementId = baseKeyboardLayoutSetElementId;
151            break;
152        }
153
154        ElementParams elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get(
155                keyboardLayoutSetElementId);
156        if (elementParams == null) {
157            elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get(
158                    KeyboardId.ELEMENT_ALPHABET);
159        }
160        // Note: The keyboard for each shift state, and mode are represented as an elementName
161        // attribute in a keyboard_layout_set XML file.  Also each keyboard layout XML resource is
162        // specified as an elementKeyboard attribute in the file.
163        // The KeyboardId is an internal key for a Keyboard object.
164        final KeyboardId id = new KeyboardId(keyboardLayoutSetElementId, mParams);
165        try {
166            return getKeyboard(elementParams, id);
167        } catch (final RuntimeException e) {
168            Log.e(TAG, "Can't create keyboard: " + id, e);
169            throw new KeyboardLayoutSetException(e, id);
170        }
171    }
172
173    private Keyboard getKeyboard(final ElementParams elementParams, final KeyboardId id) {
174        final SoftReference<Keyboard> ref = sKeyboardCache.get(id);
175        final Keyboard cachedKeyboard = (ref == null) ? null : ref.get();
176        if (cachedKeyboard != null) {
177            if (DEBUG_CACHE) {
178                Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": HIT  id=" + id);
179            }
180            return cachedKeyboard;
181        }
182
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(elementParams.mProximityCharsCorrectionEnabled);
194        final Keyboard keyboard = builder.build();
195        sKeyboardCache.put(id, new SoftReference<Keyboard>(keyboard));
196        if ((id.mElementId == KeyboardId.ELEMENT_ALPHABET
197                || id.mElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED)
198                && !mParams.mIsSpellChecker) {
199            // We only forcibly cache the primary, "ALPHABET", layouts.
200            for (int i = sForcibleKeyboardCache.length - 1; i >= 1; --i) {
201                sForcibleKeyboardCache[i] = sForcibleKeyboardCache[i - 1];
202            }
203            sForcibleKeyboardCache[0] = keyboard;
204            if (DEBUG_CACHE) {
205                Log.d(TAG, "forcing caching of keyboard with id=" + id);
206            }
207        }
208        if (DEBUG_CACHE) {
209            Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": "
210                    + ((ref == null) ? "LOAD" : "GCed") + " id=" + id);
211        }
212        return keyboard;
213    }
214
215    public static final class Builder {
216        private final Context mContext;
217        private final String mPackageName;
218        private final Resources mResources;
219
220        private final Params mParams = new Params();
221
222        private static final EditorInfo EMPTY_EDITOR_INFO = new EditorInfo();
223
224        public Builder(final Context context, final EditorInfo editorInfo) {
225            mContext = context;
226            mPackageName = context.getPackageName();
227            mResources = context.getResources();
228            final Params params = mParams;
229
230            params.mMode = getKeyboardMode(editorInfo);
231            params.mEditorInfo = (editorInfo != null) ? editorInfo : EMPTY_EDITOR_INFO;
232            params.mNoSettingsKey = InputAttributes.inPrivateImeOptions(
233                    mPackageName, NO_SETTINGS_KEY, params.mEditorInfo);
234        }
235
236        public Builder setKeyboardGeometry(final int keyboardWidth, final int keyboardHeight) {
237            mParams.mKeyboardWidth = keyboardWidth;
238            mParams.mKeyboardHeight = keyboardHeight;
239            return this;
240        }
241
242        public Builder setSubtype(final InputMethodSubtype subtype) {
243            final boolean asciiCapable = subtype.containsExtraValueKey(ASCII_CAPABLE);
244            @SuppressWarnings("deprecation")
245            final boolean deprecatedForceAscii = InputAttributes.inPrivateImeOptions(
246                    mPackageName, FORCE_ASCII, mParams.mEditorInfo);
247            final boolean forceAscii = EditorInfoCompatUtils.hasFlagForceAscii(
248                    mParams.mEditorInfo.imeOptions)
249                    || deprecatedForceAscii;
250            final InputMethodSubtype keyboardSubtype = (forceAscii && !asciiCapable)
251                    ? SubtypeSwitcher.getInstance().getNoLanguageSubtype()
252                    : subtype;
253            mParams.mSubtype = keyboardSubtype;
254            mParams.mKeyboardLayoutSetName = KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX
255                    + SubtypeLocaleUtils.getKeyboardLayoutSetName(keyboardSubtype);
256            return this;
257        }
258
259        public Builder setIsSpellChecker(final boolean isSpellChecker) {
260            mParams.mIsSpellChecker = isSpellChecker;
261            return this;
262        }
263
264        // TODO: Remove mVoiceKeyOnMain when it's certainly confirmed that we no longer show
265        // the voice input key on the symbol layout
266        public Builder setOptions(final boolean voiceKeyEnabled, final boolean voiceKeyOnMain,
267                final boolean languageSwitchKeyEnabled) {
268            @SuppressWarnings("deprecation")
269            final boolean deprecatedNoMicrophone = InputAttributes.inPrivateImeOptions(
270                    null, NO_MICROPHONE_COMPAT, mParams.mEditorInfo);
271            final boolean noMicrophone = InputAttributes.inPrivateImeOptions(
272                    mPackageName, NO_MICROPHONE, mParams.mEditorInfo)
273                    || deprecatedNoMicrophone;
274            mParams.mVoiceKeyEnabled = voiceKeyEnabled && !noMicrophone;
275            mParams.mVoiceKeyOnMain = voiceKeyOnMain;
276            mParams.mLanguageSwitchKeyEnabled = languageSwitchKeyEnabled;
277            return this;
278        }
279
280        public void disableTouchPositionCorrectionData() {
281            mParams.mDisableTouchPositionCorrectionDataForTest = true;
282        }
283
284        public KeyboardLayoutSet build() {
285            if (mParams.mSubtype == null)
286                throw new RuntimeException("KeyboardLayoutSet subtype is not specified");
287            final String packageName = mResources.getResourcePackageName(
288                    R.xml.keyboard_layout_set_qwerty);
289            final String keyboardLayoutSetName = mParams.mKeyboardLayoutSetName;
290            final int xmlId = mResources.getIdentifier(keyboardLayoutSetName, "xml", packageName);
291            try {
292                parseKeyboardLayoutSet(mResources, xmlId);
293            } catch (final IOException e) {
294                throw new RuntimeException(e.getMessage() + " in " + keyboardLayoutSetName, e);
295            } catch (final XmlPullParserException e) {
296                throw new RuntimeException(e.getMessage() + " in " + keyboardLayoutSetName, e);
297            }
298            return new KeyboardLayoutSet(mContext, mParams);
299        }
300
301        private void parseKeyboardLayoutSet(final Resources res, final int resId)
302                throws XmlPullParserException, IOException {
303            final XmlResourceParser parser = res.getXml(resId);
304            try {
305                while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
306                    final int event = parser.next();
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, TAG_KEYBOARD_SET);
313                        }
314                    }
315                }
316            } finally {
317                parser.close();
318            }
319        }
320
321        private void parseKeyboardLayoutSetContent(final XmlPullParser parser)
322                throws XmlPullParserException, IOException {
323            while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
324                final int event = parser.next();
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, 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, TAG_KEYBOARD_SET);
338                    }
339                }
340            }
341        }
342
343        private void parseKeyboardLayoutSetElement(final 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(final 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