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_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.Resources;
     27 import android.content.res.TypedArray;
     28 import android.content.res.XmlResourceParser;
     29 import android.text.InputType;
     30 import android.util.Log;
     31 import android.util.SparseArray;
     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.internal.KeyboardBuilder;
     38 import com.android.inputmethod.keyboard.internal.KeyboardParams;
     39 import com.android.inputmethod.keyboard.internal.KeysCache;
     40 import com.android.inputmethod.latin.InputAttributes;
     41 import com.android.inputmethod.latin.LatinImeLogger;
     42 import com.android.inputmethod.latin.R;
     43 import com.android.inputmethod.latin.SubtypeSwitcher;
     44 import com.android.inputmethod.latin.utils.CollectionUtils;
     45 import com.android.inputmethod.latin.utils.InputTypeUtils;
     46 import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
     47 import com.android.inputmethod.latin.utils.XmlParseUtils;
     48 
     49 import org.xmlpull.v1.XmlPullParser;
     50 import org.xmlpull.v1.XmlPullParserException;
     51 
     52 import java.io.IOException;
     53 import java.lang.ref.SoftReference;
     54 import java.util.HashMap;
     55 
     56 /**
     57  * This class represents a set of keyboard layouts. Each of them represents a different keyboard
     58  * specific to a keyboard state, such as alphabet, symbols, and so on.  Layouts in the same
     59  * {@link KeyboardLayoutSet} are related to each other.
     60  * A {@link KeyboardLayoutSet} needs to be created for each
     61  * {@link android.view.inputmethod.EditorInfo}.
     62  */
     63 public final class KeyboardLayoutSet {
     64     private static final String TAG = KeyboardLayoutSet.class.getSimpleName();
     65     private static final boolean DEBUG_CACHE = LatinImeLogger.sDBG;
     66 
     67     private static final String TAG_KEYBOARD_SET = "KeyboardLayoutSet";
     68     private static final String TAG_ELEMENT = "Element";
     69 
     70     private static final String KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX = "keyboard_layout_set_";
     71 
     72     private final Context mContext;
     73     private final Params mParams;
     74 
     75     // How many layouts we forcibly keep in cache. This only includes ALPHABET (default) and
     76     // ALPHABET_AUTOMATIC_SHIFTED layouts - other layouts may stay in memory in the map of
     77     // soft-references, but we forcibly cache this many alphabetic/auto-shifted layouts.
     78     private static final int FORCIBLE_CACHE_SIZE = 4;
     79     // By construction of soft references, anything that is also referenced somewhere else
     80     // will stay in the cache. So we forcibly keep some references in an array to prevent
     81     // them from disappearing from sKeyboardCache.
     82     private static final Keyboard[] sForcibleKeyboardCache = new Keyboard[FORCIBLE_CACHE_SIZE];
     83     private static final HashMap<KeyboardId, SoftReference<Keyboard>> sKeyboardCache =
     84             CollectionUtils.newHashMap();
     85     private static final KeysCache sKeysCache = new KeysCache();
     86 
     87     @SuppressWarnings("serial")
     88     public static final class KeyboardLayoutSetException extends RuntimeException {
     89         public final KeyboardId mKeyboardId;
     90 
     91         public KeyboardLayoutSetException(final Throwable cause, final KeyboardId keyboardId) {
     92             super(cause);
     93             mKeyboardId = keyboardId;
     94         }
     95     }
     96 
     97     private static final class ElementParams {
     98         int mKeyboardXmlId;
     99         boolean mProximityCharsCorrectionEnabled;
    100         public ElementParams() {}
    101     }
    102 
    103     public static final class Params {
    104         String mKeyboardLayoutSetName;
    105         int mMode;
    106         EditorInfo mEditorInfo;
    107         boolean mDisableTouchPositionCorrectionDataForTest;
    108         boolean mVoiceKeyEnabled;
    109         // TODO: Remove mVoiceKeyOnMain when it's certainly confirmed that we no longer show
    110         // the voice input key on the symbol layout
    111         boolean mVoiceKeyOnMain;
    112         boolean mNoSettingsKey;
    113         boolean mLanguageSwitchKeyEnabled;
    114         InputMethodSubtype mSubtype;
    115         boolean mIsSpellChecker;
    116         int mKeyboardWidth;
    117         int mKeyboardHeight;
    118         // Sparse array of KeyboardLayoutSet element parameters indexed by element's id.
    119         final SparseArray<ElementParams> mKeyboardLayoutSetElementIdToParamsMap =
    120                 CollectionUtils.newSparseArray();
    121     }
    122 
    123     public static void clearKeyboardCache() {
    124         sKeyboardCache.clear();
    125         sKeysCache.clear();
    126     }
    127 
    128     KeyboardLayoutSet(final Context context, final Params params) {
    129         mContext = context;
    130         mParams = params;
    131     }
    132 
    133     public Keyboard getKeyboard(final int baseKeyboardLayoutSetElementId) {
    134         final int keyboardLayoutSetElementId;
    135         switch (mParams.mMode) {
    136         case KeyboardId.MODE_PHONE:
    137             if (baseKeyboardLayoutSetElementId == KeyboardId.ELEMENT_SYMBOLS) {
    138                 keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE_SYMBOLS;
    139             } else {
    140                 keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE;
    141             }
    142             break;
    143         case KeyboardId.MODE_NUMBER:
    144         case KeyboardId.MODE_DATE:
    145         case KeyboardId.MODE_TIME:
    146         case KeyboardId.MODE_DATETIME:
    147             keyboardLayoutSetElementId = KeyboardId.ELEMENT_NUMBER;
    148             break;
    149         default:
    150             keyboardLayoutSetElementId = baseKeyboardLayoutSetElementId;
    151             break;
    152         }
    153 
    154         ElementParams elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get(
    155                 keyboardLayoutSetElementId);
    156         if (elementParams == null) {
    157             elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get(
    158                     KeyboardId.ELEMENT_ALPHABET);
    159         }
    160         // Note: The keyboard for each shift state, and mode are represented as an elementName
    161         // attribute in a keyboard_layout_set XML file.  Also each keyboard layout XML resource is
    162         // specified as an elementKeyboard attribute in the file.
    163         // The KeyboardId is an internal key for a Keyboard object.
    164         final KeyboardId id = new KeyboardId(keyboardLayoutSetElementId, mParams);
    165         try {
    166             return getKeyboard(elementParams, id);
    167         } catch (final RuntimeException e) {
    168             Log.e(TAG, "Can't create keyboard: " + id, e);
    169             throw new KeyboardLayoutSetException(e, id);
    170         }
    171     }
    172 
    173     private Keyboard getKeyboard(final ElementParams elementParams, final KeyboardId id) {
    174         final SoftReference<Keyboard> ref = sKeyboardCache.get(id);
    175         final Keyboard cachedKeyboard = (ref == null) ? null : ref.get();
    176         if (cachedKeyboard != null) {
    177             if (DEBUG_CACHE) {
    178                 Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": HIT  id=" + id);
    179             }
    180             return cachedKeyboard;
    181         }
    182 
    183         final KeyboardBuilder<KeyboardParams> builder =
    184                 new KeyboardBuilder<KeyboardParams>(mContext, new KeyboardParams());
    185         if (id.isAlphabetKeyboard()) {
    186             builder.setAutoGenerate(sKeysCache);
    187         }
    188         final int keyboardXmlId = elementParams.mKeyboardXmlId;
    189         builder.load(keyboardXmlId, id);
    190         if (mParams.mDisableTouchPositionCorrectionDataForTest) {
    191             builder.disableTouchPositionCorrectionDataForTest();
    192         }
    193         builder.setProximityCharsCorrectionEnabled(elementParams.mProximityCharsCorrectionEnabled);
    194         final Keyboard keyboard = builder.build();
    195         sKeyboardCache.put(id, new SoftReference<Keyboard>(keyboard));
    196         if ((id.mElementId == KeyboardId.ELEMENT_ALPHABET
    197                 || id.mElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED)
    198                 && !mParams.mIsSpellChecker) {
    199             // We only forcibly cache the primary, "ALPHABET", layouts.
    200             for (int i = sForcibleKeyboardCache.length - 1; i >= 1; --i) {
    201                 sForcibleKeyboardCache[i] = sForcibleKeyboardCache[i - 1];
    202             }
    203             sForcibleKeyboardCache[0] = keyboard;
    204             if (DEBUG_CACHE) {
    205                 Log.d(TAG, "forcing caching of keyboard with id=" + id);
    206             }
    207         }
    208         if (DEBUG_CACHE) {
    209             Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": "
    210                     + ((ref == null) ? "LOAD" : "GCed") + " id=" + id);
    211         }
    212         return keyboard;
    213     }
    214 
    215     public static final class Builder {
    216         private final Context mContext;
    217         private final String mPackageName;
    218         private final Resources mResources;
    219 
    220         private final Params mParams = new Params();
    221 
    222         private static final EditorInfo EMPTY_EDITOR_INFO = new EditorInfo();
    223 
    224         public Builder(final Context context, final EditorInfo editorInfo) {
    225             mContext = context;
    226             mPackageName = context.getPackageName();
    227             mResources = context.getResources();
    228             final Params params = mParams;
    229 
    230             params.mMode = getKeyboardMode(editorInfo);
    231             params.mEditorInfo = (editorInfo != null) ? editorInfo : EMPTY_EDITOR_INFO;
    232             params.mNoSettingsKey = InputAttributes.inPrivateImeOptions(
    233                     mPackageName, NO_SETTINGS_KEY, params.mEditorInfo);
    234         }
    235 
    236         public Builder setKeyboardGeometry(final int keyboardWidth, final int keyboardHeight) {
    237             mParams.mKeyboardWidth = keyboardWidth;
    238             mParams.mKeyboardHeight = keyboardHeight;
    239             return this;
    240         }
    241 
    242         public Builder setSubtype(final InputMethodSubtype subtype) {
    243             final boolean asciiCapable = subtype.containsExtraValueKey(ASCII_CAPABLE);
    244             @SuppressWarnings("deprecation")
    245             final boolean deprecatedForceAscii = InputAttributes.inPrivateImeOptions(
    246                     mPackageName, FORCE_ASCII, mParams.mEditorInfo);
    247             final boolean forceAscii = EditorInfoCompatUtils.hasFlagForceAscii(
    248                     mParams.mEditorInfo.imeOptions)
    249                     || deprecatedForceAscii;
    250             final InputMethodSubtype keyboardSubtype = (forceAscii && !asciiCapable)
    251                     ? SubtypeSwitcher.getInstance().getNoLanguageSubtype()
    252                     : subtype;
    253             mParams.mSubtype = keyboardSubtype;
    254             mParams.mKeyboardLayoutSetName = KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX
    255                     + SubtypeLocaleUtils.getKeyboardLayoutSetName(keyboardSubtype);
    256             return this;
    257         }
    258 
    259         public Builder setIsSpellChecker(final boolean isSpellChecker) {
    260             mParams.mIsSpellChecker = isSpellChecker;
    261             return this;
    262         }
    263 
    264         // TODO: Remove mVoiceKeyOnMain when it's certainly confirmed that we no longer show
    265         // the voice input key on the symbol layout
    266         public Builder setOptions(final boolean voiceKeyEnabled, final boolean voiceKeyOnMain,
    267                 final boolean languageSwitchKeyEnabled) {
    268             @SuppressWarnings("deprecation")
    269             final boolean deprecatedNoMicrophone = InputAttributes.inPrivateImeOptions(
    270                     null, NO_MICROPHONE_COMPAT, mParams.mEditorInfo);
    271             final boolean noMicrophone = InputAttributes.inPrivateImeOptions(
    272                     mPackageName, NO_MICROPHONE, mParams.mEditorInfo)
    273                     || deprecatedNoMicrophone;
    274             mParams.mVoiceKeyEnabled = voiceKeyEnabled && !noMicrophone;
    275             mParams.mVoiceKeyOnMain = voiceKeyOnMain;
    276             mParams.mLanguageSwitchKeyEnabled = languageSwitchKeyEnabled;
    277             return this;
    278         }
    279 
    280         public void disableTouchPositionCorrectionData() {
    281             mParams.mDisableTouchPositionCorrectionDataForTest = true;
    282         }
    283 
    284         public KeyboardLayoutSet build() {
    285             if (mParams.mSubtype == null)
    286                 throw new RuntimeException("KeyboardLayoutSet subtype is not specified");
    287             final String packageName = mResources.getResourcePackageName(
    288                     R.xml.keyboard_layout_set_qwerty);
    289             final String keyboardLayoutSetName = mParams.mKeyboardLayoutSetName;
    290             final int xmlId = mResources.getIdentifier(keyboardLayoutSetName, "xml", packageName);
    291             try {
    292                 parseKeyboardLayoutSet(mResources, xmlId);
    293             } catch (final IOException e) {
    294                 throw new RuntimeException(e.getMessage() + " in " + keyboardLayoutSetName, e);
    295             } catch (final XmlPullParserException e) {
    296                 throw new RuntimeException(e.getMessage() + " in " + keyboardLayoutSetName, e);
    297             }
    298             return new KeyboardLayoutSet(mContext, mParams);
    299         }
    300 
    301         private void parseKeyboardLayoutSet(final Resources res, final int resId)
    302                 throws XmlPullParserException, IOException {
    303             final XmlResourceParser parser = res.getXml(resId);
    304             try {
    305                 while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
    306                     final int event = parser.next();
    307                     if (event == XmlPullParser.START_TAG) {
    308                         final String tag = parser.getName();
    309                         if (TAG_KEYBOARD_SET.equals(tag)) {
    310                             parseKeyboardLayoutSetContent(parser);
    311                         } else {
    312                             throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD_SET);
    313                         }
    314                     }
    315                 }
    316             } finally {
    317                 parser.close();
    318             }
    319         }
    320 
    321         private void parseKeyboardLayoutSetContent(final XmlPullParser parser)
    322                 throws XmlPullParserException, IOException {
    323             while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
    324                 final int event = parser.next();
    325                 if (event == XmlPullParser.START_TAG) {
    326                     final String tag = parser.getName();
    327                     if (TAG_ELEMENT.equals(tag)) {
    328                         parseKeyboardLayoutSetElement(parser);
    329                     } else {
    330                         throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD_SET);
    331                     }
    332                 } else if (event == XmlPullParser.END_TAG) {
    333                     final String tag = parser.getName();
    334                     if (TAG_KEYBOARD_SET.equals(tag)) {
    335                         break;
    336                     } else {
    337                         throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_KEYBOARD_SET);
    338                     }
    339                 }
    340             }
    341         }
    342 
    343         private void parseKeyboardLayoutSetElement(final XmlPullParser parser)
    344                 throws XmlPullParserException, IOException {
    345             final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
    346                     R.styleable.KeyboardLayoutSet_Element);
    347             try {
    348                 XmlParseUtils.checkAttributeExists(a,
    349                         R.styleable.KeyboardLayoutSet_Element_elementName, "elementName",
    350                         TAG_ELEMENT, parser);
    351                 XmlParseUtils.checkAttributeExists(a,
    352                         R.styleable.KeyboardLayoutSet_Element_elementKeyboard, "elementKeyboard",
    353                         TAG_ELEMENT, parser);
    354                 XmlParseUtils.checkEndTag(TAG_ELEMENT, parser);
    355 
    356                 final ElementParams elementParams = new ElementParams();
    357                 final int elementName = a.getInt(
    358                         R.styleable.KeyboardLayoutSet_Element_elementName, 0);
    359                 elementParams.mKeyboardXmlId = a.getResourceId(
    360                         R.styleable.KeyboardLayoutSet_Element_elementKeyboard, 0);
    361                 elementParams.mProximityCharsCorrectionEnabled = a.getBoolean(
    362                         R.styleable.KeyboardLayoutSet_Element_enableProximityCharsCorrection,
    363                         false);
    364                 mParams.mKeyboardLayoutSetElementIdToParamsMap.put(elementName, elementParams);
    365             } finally {
    366                 a.recycle();
    367             }
    368         }
    369 
    370         private static int getKeyboardMode(final EditorInfo editorInfo) {
    371             if (editorInfo == null)
    372                 return KeyboardId.MODE_TEXT;
    373 
    374             final int inputType = editorInfo.inputType;
    375             final int variation = inputType & InputType.TYPE_MASK_VARIATION;
    376 
    377             switch (inputType & InputType.TYPE_MASK_CLASS) {
    378             case InputType.TYPE_CLASS_NUMBER:
    379                 return KeyboardId.MODE_NUMBER;
    380             case InputType.TYPE_CLASS_DATETIME:
    381                 switch (variation) {
    382                 case InputType.TYPE_DATETIME_VARIATION_DATE:
    383                     return KeyboardId.MODE_DATE;
    384                 case InputType.TYPE_DATETIME_VARIATION_TIME:
    385                     return KeyboardId.MODE_TIME;
    386                 default: // InputType.TYPE_DATETIME_VARIATION_NORMAL
    387                     return KeyboardId.MODE_DATETIME;
    388                 }
    389             case InputType.TYPE_CLASS_PHONE:
    390                 return KeyboardId.MODE_PHONE;
    391             case InputType.TYPE_CLASS_TEXT:
    392                 if (InputTypeUtils.isEmailVariation(variation)) {
    393                     return KeyboardId.MODE_EMAIL;
    394                 } else if (variation == InputType.TYPE_TEXT_VARIATION_URI) {
    395                     return KeyboardId.MODE_URL;
    396                 } else if (variation == InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE) {
    397                     return KeyboardId.MODE_IM;
    398                 } else if (variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
    399                     return KeyboardId.MODE_TEXT;
    400                 } else {
    401                     return KeyboardId.MODE_TEXT;
    402                 }
    403             default:
    404                 return KeyboardId.MODE_TEXT;
    405             }
    406         }
    407     }
    408 }
    409