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