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.common.Constants.ImeOption.FORCE_ASCII;
20import static com.android.inputmethod.latin.common.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.UniqueKeysCache;
38import com.android.inputmethod.latin.InputAttributes;
39import com.android.inputmethod.latin.R;
40import com.android.inputmethod.latin.RichInputMethodSubtype;
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
54import javax.annotation.Nonnull;
55import javax.annotation.Nullable;
56
57/**
58 * This class represents a set of keyboard layouts. Each of them represents a different keyboard
59 * specific to a keyboard state, such as alphabet, symbols, and so on.  Layouts in the same
60 * {@link KeyboardLayoutSet} are related to each other.
61 * A {@link KeyboardLayoutSet} needs to be created for each
62 * {@link android.view.inputmethod.EditorInfo}.
63 */
64public final class KeyboardLayoutSet {
65    private static final String TAG = KeyboardLayoutSet.class.getSimpleName();
66    private static final boolean DEBUG_CACHE = false;
67
68    private static final String TAG_KEYBOARD_SET = "KeyboardLayoutSet";
69    private static final String TAG_ELEMENT = "Element";
70    private static final String TAG_FEATURE = "Feature";
71
72    private static final String KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX = "keyboard_layout_set_";
73
74    private final Context mContext;
75    @Nonnull
76    private final Params mParams;
77
78    // How many layouts we forcibly keep in cache. This only includes ALPHABET (default) and
79    // ALPHABET_AUTOMATIC_SHIFTED layouts - other layouts may stay in memory in the map of
80    // soft-references, but we forcibly cache this many alphabetic/auto-shifted layouts.
81    private static final int FORCIBLE_CACHE_SIZE = 4;
82    // By construction of soft references, anything that is also referenced somewhere else
83    // will stay in the cache. So we forcibly keep some references in an array to prevent
84    // them from disappearing from sKeyboardCache.
85    private static final Keyboard[] sForcibleKeyboardCache = new Keyboard[FORCIBLE_CACHE_SIZE];
86    private static final HashMap<KeyboardId, SoftReference<Keyboard>> sKeyboardCache =
87            new HashMap<>();
88    @Nonnull
89    private static final UniqueKeysCache sUniqueKeysCache = UniqueKeysCache.newInstance();
90    private final static HashMap<InputMethodSubtype, Integer> sScriptIdsForSubtypes =
91            new HashMap<>();
92
93    @SuppressWarnings("serial")
94    public static final class KeyboardLayoutSetException extends RuntimeException {
95        public final KeyboardId mKeyboardId;
96
97        public KeyboardLayoutSetException(final Throwable cause, final KeyboardId keyboardId) {
98            super(cause);
99            mKeyboardId = keyboardId;
100        }
101    }
102
103    private static final class ElementParams {
104        int mKeyboardXmlId;
105        boolean mProximityCharsCorrectionEnabled;
106        boolean mSupportsSplitLayout;
107        boolean mAllowRedundantMoreKeys;
108        public ElementParams() {}
109    }
110
111    public static final class Params {
112        String mKeyboardLayoutSetName;
113        int mMode;
114        boolean mDisableTouchPositionCorrectionDataForTest;
115        // TODO: Use {@link InputAttributes} instead of these variables.
116        EditorInfo mEditorInfo;
117        boolean mIsPasswordField;
118        boolean mVoiceInputKeyEnabled;
119        boolean mNoSettingsKey;
120        boolean mLanguageSwitchKeyEnabled;
121        RichInputMethodSubtype mSubtype;
122        boolean mIsSpellChecker;
123        int mKeyboardWidth;
124        int mKeyboardHeight;
125        int mScriptId = ScriptUtils.SCRIPT_LATIN;
126        // Indicates if the user has enabled the split-layout preference
127        // and the required ProductionFlags are enabled.
128        boolean mIsSplitLayoutEnabledByUser;
129        // Indicates if split layout is actually enabled, taking into account
130        // whether the user has enabled it, and the keyboard layout supports it.
131        boolean mIsSplitLayoutEnabled;
132        // Sparse array of KeyboardLayoutSet element parameters indexed by element's id.
133        final SparseArray<ElementParams> mKeyboardLayoutSetElementIdToParamsMap =
134                new SparseArray<>();
135    }
136
137    public static void onSystemLocaleChanged() {
138        clearKeyboardCache();
139    }
140
141    public static void onKeyboardThemeChanged() {
142        clearKeyboardCache();
143    }
144
145    private static void clearKeyboardCache() {
146        sKeyboardCache.clear();
147        sUniqueKeysCache.clear();
148    }
149
150    public static int getScriptId(final Resources resources,
151            @Nonnull final InputMethodSubtype subtype) {
152        final Integer value = sScriptIdsForSubtypes.get(subtype);
153        if (null == value) {
154            final int scriptId = Builder.readScriptId(resources, subtype);
155            sScriptIdsForSubtypes.put(subtype, scriptId);
156            return scriptId;
157        }
158        return value;
159    }
160
161    KeyboardLayoutSet(final Context context, @Nonnull final Params params) {
162        mContext = context;
163        mParams = params;
164    }
165
166    @Nonnull
167    public Keyboard getKeyboard(final int baseKeyboardLayoutSetElementId) {
168        final int keyboardLayoutSetElementId;
169        switch (mParams.mMode) {
170        case KeyboardId.MODE_PHONE:
171            if (baseKeyboardLayoutSetElementId == KeyboardId.ELEMENT_SYMBOLS) {
172                keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE_SYMBOLS;
173            } else {
174                keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE;
175            }
176            break;
177        case KeyboardId.MODE_NUMBER:
178        case KeyboardId.MODE_DATE:
179        case KeyboardId.MODE_TIME:
180        case KeyboardId.MODE_DATETIME:
181            keyboardLayoutSetElementId = KeyboardId.ELEMENT_NUMBER;
182            break;
183        default:
184            keyboardLayoutSetElementId = baseKeyboardLayoutSetElementId;
185            break;
186        }
187
188        ElementParams elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get(
189                keyboardLayoutSetElementId);
190        if (elementParams == null) {
191            elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get(
192                    KeyboardId.ELEMENT_ALPHABET);
193        }
194        // Note: The keyboard for each shift state, and mode are represented as an elementName
195        // attribute in a keyboard_layout_set XML file.  Also each keyboard layout XML resource is
196        // specified as an elementKeyboard attribute in the file.
197        // The KeyboardId is an internal key for a Keyboard object.
198
199        mParams.mIsSplitLayoutEnabled = mParams.mIsSplitLayoutEnabledByUser
200                && elementParams.mSupportsSplitLayout;
201        final KeyboardId id = new KeyboardId(keyboardLayoutSetElementId, mParams);
202        try {
203            return getKeyboard(elementParams, id);
204        } catch (final RuntimeException e) {
205            Log.e(TAG, "Can't create keyboard: " + id, e);
206            throw new KeyboardLayoutSetException(e, id);
207        }
208    }
209
210    @Nonnull
211    private Keyboard getKeyboard(final ElementParams elementParams, final KeyboardId id) {
212        final SoftReference<Keyboard> ref = sKeyboardCache.get(id);
213        final Keyboard cachedKeyboard = (ref == null) ? null : ref.get();
214        if (cachedKeyboard != null) {
215            if (DEBUG_CACHE) {
216                Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": HIT  id=" + id);
217            }
218            return cachedKeyboard;
219        }
220
221        final KeyboardBuilder<KeyboardParams> builder =
222                new KeyboardBuilder<>(mContext, new KeyboardParams(sUniqueKeysCache));
223        sUniqueKeysCache.setEnabled(id.isAlphabetKeyboard());
224        builder.setAllowRedundantMoreKes(elementParams.mAllowRedundantMoreKeys);
225        final int keyboardXmlId = elementParams.mKeyboardXmlId;
226        builder.load(keyboardXmlId, id);
227        if (mParams.mDisableTouchPositionCorrectionDataForTest) {
228            builder.disableTouchPositionCorrectionDataForTest();
229        }
230        builder.setProximityCharsCorrectionEnabled(elementParams.mProximityCharsCorrectionEnabled);
231        final Keyboard keyboard = builder.build();
232        sKeyboardCache.put(id, new SoftReference<>(keyboard));
233        if ((id.mElementId == KeyboardId.ELEMENT_ALPHABET
234                || id.mElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED)
235                && !mParams.mIsSpellChecker) {
236            // We only forcibly cache the primary, "ALPHABET", layouts.
237            for (int i = sForcibleKeyboardCache.length - 1; i >= 1; --i) {
238                sForcibleKeyboardCache[i] = sForcibleKeyboardCache[i - 1];
239            }
240            sForcibleKeyboardCache[0] = keyboard;
241            if (DEBUG_CACHE) {
242                Log.d(TAG, "forcing caching of keyboard with id=" + id);
243            }
244        }
245        if (DEBUG_CACHE) {
246            Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": "
247                    + ((ref == null) ? "LOAD" : "GCed") + " id=" + id);
248        }
249        return keyboard;
250    }
251
252    public int getScriptId() {
253        return mParams.mScriptId;
254    }
255
256    public static final class Builder {
257        private final Context mContext;
258        private final String mPackageName;
259        private final Resources mResources;
260
261        private final Params mParams = new Params();
262
263        private static final EditorInfo EMPTY_EDITOR_INFO = new EditorInfo();
264
265        public Builder(final Context context, @Nullable final EditorInfo ei) {
266            mContext = context;
267            mPackageName = context.getPackageName();
268            mResources = context.getResources();
269            final Params params = mParams;
270
271            final EditorInfo editorInfo = (ei != null) ? ei : EMPTY_EDITOR_INFO;
272            params.mMode = getKeyboardMode(editorInfo);
273            // TODO: Consolidate those with {@link InputAttributes}.
274            params.mEditorInfo = editorInfo;
275            params.mIsPasswordField = InputTypeUtils.isPasswordInputType(editorInfo.inputType);
276            params.mNoSettingsKey = InputAttributes.inPrivateImeOptions(
277                    mPackageName, NO_SETTINGS_KEY, editorInfo);
278        }
279
280        public Builder setKeyboardGeometry(final int keyboardWidth, final int keyboardHeight) {
281            mParams.mKeyboardWidth = keyboardWidth;
282            mParams.mKeyboardHeight = keyboardHeight;
283            return this;
284        }
285
286        public Builder setSubtype(@Nonnull final RichInputMethodSubtype subtype) {
287            final boolean asciiCapable = InputMethodSubtypeCompatUtils.isAsciiCapable(subtype);
288            // TODO: Consolidate with {@link InputAttributes}.
289            @SuppressWarnings("deprecation")
290            final boolean deprecatedForceAscii = InputAttributes.inPrivateImeOptions(
291                    mPackageName, FORCE_ASCII, mParams.mEditorInfo);
292            final boolean forceAscii = EditorInfoCompatUtils.hasFlagForceAscii(
293                    mParams.mEditorInfo.imeOptions)
294                    || deprecatedForceAscii;
295            final RichInputMethodSubtype keyboardSubtype = (forceAscii && !asciiCapable)
296                    ? RichInputMethodSubtype.getNoLanguageSubtype()
297                    : subtype;
298            mParams.mSubtype = keyboardSubtype;
299            mParams.mKeyboardLayoutSetName = KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX
300                    + keyboardSubtype.getKeyboardLayoutSetName();
301            return this;
302        }
303
304        public Builder setIsSpellChecker(final boolean isSpellChecker) {
305            mParams.mIsSpellChecker = isSpellChecker;
306            return this;
307        }
308
309        public Builder setVoiceInputKeyEnabled(final boolean enabled) {
310            mParams.mVoiceInputKeyEnabled = enabled;
311            return this;
312        }
313
314        public Builder setLanguageSwitchKeyEnabled(final boolean enabled) {
315            mParams.mLanguageSwitchKeyEnabled = enabled;
316            return this;
317        }
318
319        public Builder disableTouchPositionCorrectionData() {
320            mParams.mDisableTouchPositionCorrectionDataForTest = true;
321            return this;
322        }
323
324        public Builder setSplitLayoutEnabledByUser(final boolean enabled) {
325            mParams.mIsSplitLayoutEnabledByUser = enabled;
326            return this;
327        }
328
329        // Super redux version of reading the script ID for some subtype from Xml.
330        static int readScriptId(final Resources resources, final InputMethodSubtype subtype) {
331            final String layoutSetName = KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX
332                    + SubtypeLocaleUtils.getKeyboardLayoutSetName(subtype);
333            final int xmlId = getXmlId(resources, layoutSetName);
334            final XmlResourceParser parser = resources.getXml(xmlId);
335            try {
336                while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
337                    // Bovinate through the XML stupidly searching for TAG_FEATURE, and read
338                    // the script Id from it.
339                    parser.next();
340                    final String tag = parser.getName();
341                    if (TAG_FEATURE.equals(tag)) {
342                        return readScriptIdFromTagFeature(resources, parser);
343                    }
344                }
345            } catch (final IOException | XmlPullParserException e) {
346                throw new RuntimeException(e.getMessage() + " in " + layoutSetName, e);
347            } finally {
348                parser.close();
349            }
350            // If the tag is not found, then the default script is Latin.
351            return ScriptUtils.SCRIPT_LATIN;
352        }
353
354        private static int readScriptIdFromTagFeature(final Resources resources,
355                final XmlPullParser parser) throws IOException, XmlPullParserException {
356            final TypedArray featureAttr = resources.obtainAttributes(Xml.asAttributeSet(parser),
357                    R.styleable.KeyboardLayoutSet_Feature);
358            try {
359                final int scriptId =
360                        featureAttr.getInt(R.styleable.KeyboardLayoutSet_Feature_supportedScript,
361                                ScriptUtils.SCRIPT_UNKNOWN);
362                XmlParseUtils.checkEndTag(TAG_FEATURE, parser);
363                return scriptId;
364            } finally {
365                featureAttr.recycle();
366            }
367        }
368
369        public KeyboardLayoutSet build() {
370            if (mParams.mSubtype == null)
371                throw new RuntimeException("KeyboardLayoutSet subtype is not specified");
372            final int xmlId = getXmlId(mResources, mParams.mKeyboardLayoutSetName);
373            try {
374                parseKeyboardLayoutSet(mResources, xmlId);
375            } catch (final IOException | XmlPullParserException e) {
376                throw new RuntimeException(e.getMessage() + " in " + mParams.mKeyboardLayoutSetName,
377                        e);
378            }
379            return new KeyboardLayoutSet(mContext, mParams);
380        }
381
382        private static int getXmlId(final Resources resources, final String keyboardLayoutSetName) {
383            final String packageName = resources.getResourcePackageName(
384                    R.xml.keyboard_layout_set_qwerty);
385            return resources.getIdentifier(keyboardLayoutSetName, "xml", packageName);
386        }
387
388        private void parseKeyboardLayoutSet(final Resources res, final int resId)
389                throws XmlPullParserException, IOException {
390            final XmlResourceParser parser = res.getXml(resId);
391            try {
392                while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
393                    final int event = parser.next();
394                    if (event == XmlPullParser.START_TAG) {
395                        final String tag = parser.getName();
396                        if (TAG_KEYBOARD_SET.equals(tag)) {
397                            parseKeyboardLayoutSetContent(parser);
398                        } else {
399                            throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD_SET);
400                        }
401                    }
402                }
403            } finally {
404                parser.close();
405            }
406        }
407
408        private void parseKeyboardLayoutSetContent(final XmlPullParser parser)
409                throws XmlPullParserException, IOException {
410            while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
411                final int event = parser.next();
412                if (event == XmlPullParser.START_TAG) {
413                    final String tag = parser.getName();
414                    if (TAG_ELEMENT.equals(tag)) {
415                        parseKeyboardLayoutSetElement(parser);
416                    } else if (TAG_FEATURE.equals(tag)) {
417                        mParams.mScriptId = readScriptIdFromTagFeature(mResources, parser);
418                    } else {
419                        throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD_SET);
420                    }
421                } else if (event == XmlPullParser.END_TAG) {
422                    final String tag = parser.getName();
423                    if (TAG_KEYBOARD_SET.equals(tag)) {
424                        break;
425                    }
426                    throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_KEYBOARD_SET);
427                }
428            }
429        }
430
431        private void parseKeyboardLayoutSetElement(final XmlPullParser parser)
432                throws XmlPullParserException, IOException {
433            final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
434                    R.styleable.KeyboardLayoutSet_Element);
435            try {
436                XmlParseUtils.checkAttributeExists(a,
437                        R.styleable.KeyboardLayoutSet_Element_elementName, "elementName",
438                        TAG_ELEMENT, parser);
439                XmlParseUtils.checkAttributeExists(a,
440                        R.styleable.KeyboardLayoutSet_Element_elementKeyboard, "elementKeyboard",
441                        TAG_ELEMENT, parser);
442                XmlParseUtils.checkEndTag(TAG_ELEMENT, parser);
443
444                final ElementParams elementParams = new ElementParams();
445                final int elementName = a.getInt(
446                        R.styleable.KeyboardLayoutSet_Element_elementName, 0);
447                elementParams.mKeyboardXmlId = a.getResourceId(
448                        R.styleable.KeyboardLayoutSet_Element_elementKeyboard, 0);
449                elementParams.mProximityCharsCorrectionEnabled = a.getBoolean(
450                        R.styleable.KeyboardLayoutSet_Element_enableProximityCharsCorrection,
451                        false);
452                elementParams.mSupportsSplitLayout = a.getBoolean(
453                        R.styleable.KeyboardLayoutSet_Element_supportsSplitLayout, false);
454                elementParams.mAllowRedundantMoreKeys = a.getBoolean(
455                        R.styleable.KeyboardLayoutSet_Element_allowRedundantMoreKeys, true);
456                mParams.mKeyboardLayoutSetElementIdToParamsMap.put(elementName, elementParams);
457            } finally {
458                a.recycle();
459            }
460        }
461
462        private static int getKeyboardMode(final EditorInfo editorInfo) {
463            final int inputType = editorInfo.inputType;
464            final int variation = inputType & InputType.TYPE_MASK_VARIATION;
465
466            switch (inputType & InputType.TYPE_MASK_CLASS) {
467            case InputType.TYPE_CLASS_NUMBER:
468                return KeyboardId.MODE_NUMBER;
469            case InputType.TYPE_CLASS_DATETIME:
470                switch (variation) {
471                case InputType.TYPE_DATETIME_VARIATION_DATE:
472                    return KeyboardId.MODE_DATE;
473                case InputType.TYPE_DATETIME_VARIATION_TIME:
474                    return KeyboardId.MODE_TIME;
475                default: // InputType.TYPE_DATETIME_VARIATION_NORMAL
476                    return KeyboardId.MODE_DATETIME;
477                }
478            case InputType.TYPE_CLASS_PHONE:
479                return KeyboardId.MODE_PHONE;
480            case InputType.TYPE_CLASS_TEXT:
481                if (InputTypeUtils.isEmailVariation(variation)) {
482                    return KeyboardId.MODE_EMAIL;
483                } else if (variation == InputType.TYPE_TEXT_VARIATION_URI) {
484                    return KeyboardId.MODE_URL;
485                } else if (variation == InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE) {
486                    return KeyboardId.MODE_IM;
487                } else if (variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
488                    return KeyboardId.MODE_TEXT;
489                } else {
490                    return KeyboardId.MODE_TEXT;
491                }
492            default:
493                return KeyboardId.MODE_TEXT;
494            }
495        }
496    }
497}
498