Home | History | Annotate | Download | only in keyboard
      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 
     17 package com.android.inputmethod.keyboard;
     18 
     19 import static com.android.inputmethod.latin.common.Constants.ImeOption.FORCE_ASCII;
     20 import static com.android.inputmethod.latin.common.Constants.ImeOption.NO_SETTINGS_KEY;
     21 
     22 import android.content.Context;
     23 import android.content.res.Resources;
     24 import android.content.res.TypedArray;
     25 import android.content.res.XmlResourceParser;
     26 import android.text.InputType;
     27 import android.util.Log;
     28 import android.util.SparseArray;
     29 import android.util.Xml;
     30 import android.view.inputmethod.EditorInfo;
     31 import android.view.inputmethod.InputMethodSubtype;
     32 
     33 import com.android.inputmethod.compat.EditorInfoCompatUtils;
     34 import com.android.inputmethod.compat.InputMethodSubtypeCompatUtils;
     35 import com.android.inputmethod.keyboard.internal.KeyboardBuilder;
     36 import com.android.inputmethod.keyboard.internal.KeyboardParams;
     37 import com.android.inputmethod.keyboard.internal.UniqueKeysCache;
     38 import com.android.inputmethod.latin.InputAttributes;
     39 import com.android.inputmethod.latin.R;
     40 import com.android.inputmethod.latin.RichInputMethodSubtype;
     41 import com.android.inputmethod.latin.define.DebugFlags;
     42 import com.android.inputmethod.latin.utils.InputTypeUtils;
     43 import com.android.inputmethod.latin.utils.ScriptUtils;
     44 import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
     45 import com.android.inputmethod.latin.utils.XmlParseUtils;
     46 
     47 import org.xmlpull.v1.XmlPullParser;
     48 import org.xmlpull.v1.XmlPullParserException;
     49 
     50 import java.io.IOException;
     51 import java.lang.ref.SoftReference;
     52 import java.util.HashMap;
     53 
     54 import javax.annotation.Nonnull;
     55 import 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  */
     64 public 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