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.Constants.ImeOption.FORCE_ASCII;
     20 import static com.android.inputmethod.latin.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.KeysCache;
     38 import com.android.inputmethod.latin.InputAttributes;
     39 import com.android.inputmethod.latin.R;
     40 import com.android.inputmethod.latin.SubtypeSwitcher;
     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 /**
     55  * This class represents a set of keyboard layouts. Each of them represents a different keyboard
     56  * specific to a keyboard state, such as alphabet, symbols, and so on.  Layouts in the same
     57  * {@link KeyboardLayoutSet} are related to each other.
     58  * A {@link KeyboardLayoutSet} needs to be created for each
     59  * {@link android.view.inputmethod.EditorInfo}.
     60  */
     61 public final class KeyboardLayoutSet {
     62     private static final String TAG = KeyboardLayoutSet.class.getSimpleName();
     63     private static final boolean DEBUG_CACHE = DebugFlags.DEBUG_ENABLED;
     64 
     65     private static final String TAG_KEYBOARD_SET = "KeyboardLayoutSet";
     66     private static final String TAG_ELEMENT = "Element";
     67     private static final String TAG_FEATURE = "Feature";
     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     // How many layouts we forcibly keep in cache. This only includes ALPHABET (default) and
     75     // ALPHABET_AUTOMATIC_SHIFTED layouts - other layouts may stay in memory in the map of
     76     // soft-references, but we forcibly cache this many alphabetic/auto-shifted layouts.
     77     private static final int FORCIBLE_CACHE_SIZE = 4;
     78     // By construction of soft references, anything that is also referenced somewhere else
     79     // will stay in the cache. So we forcibly keep some references in an array to prevent
     80     // them from disappearing from sKeyboardCache.
     81     private static final Keyboard[] sForcibleKeyboardCache = new Keyboard[FORCIBLE_CACHE_SIZE];
     82     private static final HashMap<KeyboardId, SoftReference<Keyboard>> sKeyboardCache =
     83             new HashMap<>();
     84     private static final KeysCache sKeysCache = new KeysCache();
     85 
     86     @SuppressWarnings("serial")
     87     public static final class KeyboardLayoutSetException extends RuntimeException {
     88         public final KeyboardId mKeyboardId;
     89 
     90         public KeyboardLayoutSetException(final Throwable cause, final KeyboardId keyboardId) {
     91             super(cause);
     92             mKeyboardId = keyboardId;
     93         }
     94     }
     95 
     96     private static final class ElementParams {
     97         int mKeyboardXmlId;
     98         boolean mProximityCharsCorrectionEnabled;
     99         public ElementParams() {}
    100     }
    101 
    102     public static final class Params {
    103         String mKeyboardLayoutSetName;
    104         int mMode;
    105         boolean mDisableTouchPositionCorrectionDataForTest;
    106         // TODO: Use {@link InputAttributes} instead of these variables.
    107         EditorInfo mEditorInfo;
    108         boolean mIsPasswordField;
    109         boolean mVoiceInputKeyEnabled;
    110         boolean mNoSettingsKey;
    111         boolean mLanguageSwitchKeyEnabled;
    112         InputMethodSubtype mSubtype;
    113         boolean mIsSpellChecker;
    114         int mKeyboardWidth;
    115         int mKeyboardHeight;
    116         int mScriptId = ScriptUtils.SCRIPT_LATIN;
    117         // Sparse array of KeyboardLayoutSet element parameters indexed by element's id.
    118         final SparseArray<ElementParams> mKeyboardLayoutSetElementIdToParamsMap =
    119                 new SparseArray<>();
    120     }
    121 
    122     public static void onSystemLocaleChanged() {
    123         clearKeyboardCache();
    124     }
    125 
    126     public static void onKeyboardThemeChanged() {
    127         clearKeyboardCache();
    128     }
    129 
    130     private 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 (final RuntimeException e) {
    175             Log.e(TAG, "Can't create keyboard: " + id, e);
    176             throw new KeyboardLayoutSetException(e, id);
    177         }
    178     }
    179 
    180     private Keyboard getKeyboard(final ElementParams elementParams, final KeyboardId id) {
    181         final SoftReference<Keyboard> ref = sKeyboardCache.get(id);
    182         final Keyboard cachedKeyboard = (ref == null) ? null : ref.get();
    183         if (cachedKeyboard != null) {
    184             if (DEBUG_CACHE) {
    185                 Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": HIT  id=" + id);
    186             }
    187             return cachedKeyboard;
    188         }
    189 
    190         final KeyboardBuilder<KeyboardParams> builder =
    191                 new KeyboardBuilder<>(mContext, new KeyboardParams());
    192         if (id.isAlphabetKeyboard()) {
    193             builder.setAutoGenerate(sKeysCache);
    194         }
    195         final int keyboardXmlId = elementParams.mKeyboardXmlId;
    196         builder.load(keyboardXmlId, id);
    197         if (mParams.mDisableTouchPositionCorrectionDataForTest) {
    198             builder.disableTouchPositionCorrectionDataForTest();
    199         }
    200         builder.setProximityCharsCorrectionEnabled(elementParams.mProximityCharsCorrectionEnabled);
    201         final Keyboard keyboard = builder.build();
    202         sKeyboardCache.put(id, new SoftReference<>(keyboard));
    203         if ((id.mElementId == KeyboardId.ELEMENT_ALPHABET
    204                 || id.mElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED)
    205                 && !mParams.mIsSpellChecker) {
    206             // We only forcibly cache the primary, "ALPHABET", layouts.
    207             for (int i = sForcibleKeyboardCache.length - 1; i >= 1; --i) {
    208                 sForcibleKeyboardCache[i] = sForcibleKeyboardCache[i - 1];
    209             }
    210             sForcibleKeyboardCache[0] = keyboard;
    211             if (DEBUG_CACHE) {
    212                 Log.d(TAG, "forcing caching of keyboard with id=" + id);
    213             }
    214         }
    215         if (DEBUG_CACHE) {
    216             Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": "
    217                     + ((ref == null) ? "LOAD" : "GCed") + " id=" + id);
    218         }
    219         return keyboard;
    220     }
    221 
    222     public int getScriptId() {
    223         return mParams.mScriptId;
    224     }
    225 
    226     public static final class Builder {
    227         private final Context mContext;
    228         private final String mPackageName;
    229         private final Resources mResources;
    230 
    231         private final Params mParams = new Params();
    232 
    233         private static final EditorInfo EMPTY_EDITOR_INFO = new EditorInfo();
    234 
    235         public Builder(final Context context, final EditorInfo ei) {
    236             mContext = context;
    237             mPackageName = context.getPackageName();
    238             mResources = context.getResources();
    239             final Params params = mParams;
    240 
    241             final EditorInfo editorInfo = (ei != null) ? ei : EMPTY_EDITOR_INFO;
    242             params.mMode = getKeyboardMode(editorInfo);
    243             // TODO: Consolidate those with {@link InputAttributes}.
    244             params.mEditorInfo = editorInfo;
    245             params.mIsPasswordField = InputTypeUtils.isPasswordInputType(editorInfo.inputType);
    246             params.mNoSettingsKey = InputAttributes.inPrivateImeOptions(
    247                     mPackageName, NO_SETTINGS_KEY, editorInfo);
    248         }
    249 
    250         public Builder setKeyboardGeometry(final int keyboardWidth, final int keyboardHeight) {
    251             mParams.mKeyboardWidth = keyboardWidth;
    252             mParams.mKeyboardHeight = keyboardHeight;
    253             return this;
    254         }
    255 
    256         public Builder setSubtype(final InputMethodSubtype subtype) {
    257             final boolean asciiCapable = InputMethodSubtypeCompatUtils.isAsciiCapable(subtype);
    258             // TODO: Consolidate with {@link InputAttributes}.
    259             @SuppressWarnings("deprecation")
    260             final boolean deprecatedForceAscii = InputAttributes.inPrivateImeOptions(
    261                     mPackageName, FORCE_ASCII, mParams.mEditorInfo);
    262             final boolean forceAscii = EditorInfoCompatUtils.hasFlagForceAscii(
    263                     mParams.mEditorInfo.imeOptions)
    264                     || deprecatedForceAscii;
    265             final InputMethodSubtype keyboardSubtype = (forceAscii && !asciiCapable)
    266                     ? SubtypeSwitcher.getInstance().getNoLanguageSubtype()
    267                     : subtype;
    268             mParams.mSubtype = keyboardSubtype;
    269             mParams.mKeyboardLayoutSetName = KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX
    270                     + SubtypeLocaleUtils.getKeyboardLayoutSetName(keyboardSubtype);
    271             return this;
    272         }
    273 
    274         public Builder setIsSpellChecker(final boolean isSpellChecker) {
    275             mParams.mIsSpellChecker = isSpellChecker;
    276             return this;
    277         }
    278 
    279         public Builder setVoiceInputKeyEnabled(final boolean enabled) {
    280             mParams.mVoiceInputKeyEnabled = enabled;
    281             return this;
    282         }
    283 
    284         public Builder setLanguageSwitchKeyEnabled(final boolean enabled) {
    285             mParams.mLanguageSwitchKeyEnabled = enabled;
    286             return this;
    287         }
    288 
    289         public void disableTouchPositionCorrectionData() {
    290             mParams.mDisableTouchPositionCorrectionDataForTest = true;
    291         }
    292 
    293         public void setScriptId(final int scriptId) {
    294             mParams.mScriptId = scriptId;
    295         }
    296 
    297         public KeyboardLayoutSet build() {
    298             if (mParams.mSubtype == null)
    299                 throw new RuntimeException("KeyboardLayoutSet subtype is not specified");
    300             final String packageName = mResources.getResourcePackageName(
    301                     R.xml.keyboard_layout_set_qwerty);
    302             final String keyboardLayoutSetName = mParams.mKeyboardLayoutSetName;
    303             final int xmlId = mResources.getIdentifier(keyboardLayoutSetName, "xml", packageName);
    304             try {
    305                 parseKeyboardLayoutSet(mResources, xmlId);
    306             } catch (final IOException e) {
    307                 throw new RuntimeException(e.getMessage() + " in " + keyboardLayoutSetName, e);
    308             } catch (final XmlPullParserException e) {
    309                 throw new RuntimeException(e.getMessage() + " in " + keyboardLayoutSetName, e);
    310             }
    311             return new KeyboardLayoutSet(mContext, mParams);
    312         }
    313 
    314         private void parseKeyboardLayoutSet(final Resources res, final int resId)
    315                 throws XmlPullParserException, IOException {
    316             final XmlResourceParser parser = res.getXml(resId);
    317             try {
    318                 while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
    319                     final int event = parser.next();
    320                     if (event == XmlPullParser.START_TAG) {
    321                         final String tag = parser.getName();
    322                         if (TAG_KEYBOARD_SET.equals(tag)) {
    323                             parseKeyboardLayoutSetContent(parser);
    324                         } else {
    325                             throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD_SET);
    326                         }
    327                     }
    328                 }
    329             } finally {
    330                 parser.close();
    331             }
    332         }
    333 
    334         private void parseKeyboardLayoutSetContent(final XmlPullParser parser)
    335                 throws XmlPullParserException, IOException {
    336             while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
    337                 final int event = parser.next();
    338                 if (event == XmlPullParser.START_TAG) {
    339                     final String tag = parser.getName();
    340                     if (TAG_ELEMENT.equals(tag)) {
    341                         parseKeyboardLayoutSetElement(parser);
    342                     } else if (TAG_FEATURE.equals(tag)) {
    343                         parseKeyboardLayoutSetFeature(parser);
    344                     } else {
    345                         throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD_SET);
    346                     }
    347                 } else if (event == XmlPullParser.END_TAG) {
    348                     final String tag = parser.getName();
    349                     if (TAG_KEYBOARD_SET.equals(tag)) {
    350                         break;
    351                     } else {
    352                         throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_KEYBOARD_SET);
    353                     }
    354                 }
    355             }
    356         }
    357 
    358         private void parseKeyboardLayoutSetElement(final XmlPullParser parser)
    359                 throws XmlPullParserException, IOException {
    360             final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
    361                     R.styleable.KeyboardLayoutSet_Element);
    362             try {
    363                 XmlParseUtils.checkAttributeExists(a,
    364                         R.styleable.KeyboardLayoutSet_Element_elementName, "elementName",
    365                         TAG_ELEMENT, parser);
    366                 XmlParseUtils.checkAttributeExists(a,
    367                         R.styleable.KeyboardLayoutSet_Element_elementKeyboard, "elementKeyboard",
    368                         TAG_ELEMENT, parser);
    369                 XmlParseUtils.checkEndTag(TAG_ELEMENT, parser);
    370 
    371                 final ElementParams elementParams = new ElementParams();
    372                 final int elementName = a.getInt(
    373                         R.styleable.KeyboardLayoutSet_Element_elementName, 0);
    374                 elementParams.mKeyboardXmlId = a.getResourceId(
    375                         R.styleable.KeyboardLayoutSet_Element_elementKeyboard, 0);
    376                 elementParams.mProximityCharsCorrectionEnabled = a.getBoolean(
    377                         R.styleable.KeyboardLayoutSet_Element_enableProximityCharsCorrection,
    378                         false);
    379                 mParams.mKeyboardLayoutSetElementIdToParamsMap.put(elementName, elementParams);
    380             } finally {
    381                 a.recycle();
    382             }
    383         }
    384 
    385         private void parseKeyboardLayoutSetFeature(final XmlPullParser parser)
    386                 throws XmlPullParserException, IOException {
    387             final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
    388                     R.styleable.KeyboardLayoutSet_Feature);
    389             try {
    390                 final int scriptId = a.getInt(
    391                         R.styleable.KeyboardLayoutSet_Feature_supportedScript,
    392                         ScriptUtils.SCRIPT_LATIN);
    393                 XmlParseUtils.checkEndTag(TAG_FEATURE, parser);
    394                 setScriptId(scriptId);
    395             } finally {
    396                 a.recycle();
    397             }
    398         }
    399 
    400         private static int getKeyboardMode(final EditorInfo editorInfo) {
    401             final int inputType = editorInfo.inputType;
    402             final int variation = inputType & InputType.TYPE_MASK_VARIATION;
    403 
    404             switch (inputType & InputType.TYPE_MASK_CLASS) {
    405             case InputType.TYPE_CLASS_NUMBER:
    406                 return KeyboardId.MODE_NUMBER;
    407             case InputType.TYPE_CLASS_DATETIME:
    408                 switch (variation) {
    409                 case InputType.TYPE_DATETIME_VARIATION_DATE:
    410                     return KeyboardId.MODE_DATE;
    411                 case InputType.TYPE_DATETIME_VARIATION_TIME:
    412                     return KeyboardId.MODE_TIME;
    413                 default: // InputType.TYPE_DATETIME_VARIATION_NORMAL
    414                     return KeyboardId.MODE_DATETIME;
    415                 }
    416             case InputType.TYPE_CLASS_PHONE:
    417                 return KeyboardId.MODE_PHONE;
    418             case InputType.TYPE_CLASS_TEXT:
    419                 if (InputTypeUtils.isEmailVariation(variation)) {
    420                     return KeyboardId.MODE_EMAIL;
    421                 } else if (variation == InputType.TYPE_TEXT_VARIATION_URI) {
    422                     return KeyboardId.MODE_URL;
    423                 } else if (variation == InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE) {
    424                     return KeyboardId.MODE_IM;
    425                 } else if (variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
    426                     return KeyboardId.MODE_TEXT;
    427                 } else {
    428                     return KeyboardId.MODE_TEXT;
    429                 }
    430             default:
    431                 return KeyboardId.MODE_TEXT;
    432             }
    433         }
    434     }
    435 }
    436