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.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.text.TextUtils;
     32 import android.util.DisplayMetrics;
     33 import android.util.Log;
     34 import android.util.SparseArray;
     35 import android.util.Xml;
     36 import android.view.inputmethod.EditorInfo;
     37 import android.view.inputmethod.InputMethodSubtype;
     38 
     39 import com.android.inputmethod.compat.EditorInfoCompatUtils;
     40 import com.android.inputmethod.keyboard.internal.KeyboardBuilder;
     41 import com.android.inputmethod.keyboard.internal.KeyboardParams;
     42 import com.android.inputmethod.keyboard.internal.KeysCache;
     43 import com.android.inputmethod.latin.AdditionalSubtype;
     44 import com.android.inputmethod.latin.CollectionUtils;
     45 import com.android.inputmethod.latin.InputAttributes;
     46 import com.android.inputmethod.latin.InputTypeUtils;
     47 import com.android.inputmethod.latin.LatinImeLogger;
     48 import com.android.inputmethod.latin.R;
     49 import com.android.inputmethod.latin.ResourceUtils;
     50 import com.android.inputmethod.latin.SubtypeLocale;
     51 import com.android.inputmethod.latin.SubtypeSwitcher;
     52 import com.android.inputmethod.latin.XmlParseUtils;
     53 
     54 import org.xmlpull.v1.XmlPullParser;
     55 import org.xmlpull.v1.XmlPullParserException;
     56 
     57 import java.io.IOException;
     58 import java.lang.ref.SoftReference;
     59 import java.util.HashMap;
     60 
     61 /**
     62  * This class represents a set of keyboard layouts. Each of them represents a different keyboard
     63  * specific to a keyboard state, such as alphabet, symbols, and so on.  Layouts in the same
     64  * {@link KeyboardLayoutSet} are related to each other.
     65  * A {@link KeyboardLayoutSet} needs to be created for each
     66  * {@link android.view.inputmethod.EditorInfo}.
     67  */
     68 public final class KeyboardLayoutSet {
     69     private static final String TAG = KeyboardLayoutSet.class.getSimpleName();
     70     private static final boolean DEBUG_CACHE = LatinImeLogger.sDBG;
     71 
     72     private static final String TAG_KEYBOARD_SET = "KeyboardLayoutSet";
     73     private static final String TAG_ELEMENT = "Element";
     74 
     75     private static final String KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX = "keyboard_layout_set_";
     76     private static final int SPELLCHECKER_DUMMY_KEYBOARD_WIDTH = 480;
     77     private static final int SPELLCHECKER_DUMMY_KEYBOARD_HEIGHT = 800;
     78 
     79     private final Context mContext;
     80     private final Params mParams;
     81 
     82     private static final HashMap<KeyboardId, SoftReference<Keyboard>> sKeyboardCache =
     83             CollectionUtils.newHashMap();
     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         EditorInfo mEditorInfo;
    106         boolean mDisableTouchPositionCorrectionDataForTest;
    107         boolean mVoiceKeyEnabled;
    108         boolean mVoiceKeyOnMain;
    109         boolean mNoSettingsKey;
    110         boolean mLanguageSwitchKeyEnabled;
    111         InputMethodSubtype mSubtype;
    112         int mOrientation;
    113         int mKeyboardWidth;
    114         int mKeyboardHeight;
    115         // Sparse array of KeyboardLayoutSet element parameters indexed by element's id.
    116         final SparseArray<ElementParams> mKeyboardLayoutSetElementIdToParamsMap =
    117                 CollectionUtils.newSparseArray();
    118     }
    119 
    120     public static void clearKeyboardCache() {
    121         sKeyboardCache.clear();
    122         sKeysCache.clear();
    123     }
    124 
    125     KeyboardLayoutSet(final Context context, final Params params) {
    126         mContext = context;
    127         mParams = params;
    128     }
    129 
    130     public Keyboard getKeyboard(final int baseKeyboardLayoutSetElementId) {
    131         final int keyboardLayoutSetElementId;
    132         switch (mParams.mMode) {
    133         case KeyboardId.MODE_PHONE:
    134             if (baseKeyboardLayoutSetElementId == KeyboardId.ELEMENT_SYMBOLS) {
    135                 keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE_SYMBOLS;
    136             } else {
    137                 keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE;
    138             }
    139             break;
    140         case KeyboardId.MODE_NUMBER:
    141         case KeyboardId.MODE_DATE:
    142         case KeyboardId.MODE_TIME:
    143         case KeyboardId.MODE_DATETIME:
    144             keyboardLayoutSetElementId = KeyboardId.ELEMENT_NUMBER;
    145             break;
    146         default:
    147             keyboardLayoutSetElementId = baseKeyboardLayoutSetElementId;
    148             break;
    149         }
    150 
    151         ElementParams elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get(
    152                 keyboardLayoutSetElementId);
    153         if (elementParams == null) {
    154             elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get(
    155                     KeyboardId.ELEMENT_ALPHABET);
    156         }
    157         // Note: The keyboard for each shift state, and mode are represented as an elementName
    158         // attribute in a keyboard_layout_set XML file.  Also each keyboard layout XML resource is
    159         // specified as an elementKeyboard attribute in the file.
    160         // The KeyboardId is an internal key for a Keyboard object.
    161         final KeyboardId id = new KeyboardId(keyboardLayoutSetElementId, mParams);
    162         try {
    163             return getKeyboard(elementParams, id);
    164         } catch (RuntimeException e) {
    165             throw new KeyboardLayoutSetException(e, id);
    166         }
    167     }
    168 
    169     private Keyboard getKeyboard(final ElementParams elementParams, final KeyboardId id) {
    170         final SoftReference<Keyboard> ref = sKeyboardCache.get(id);
    171         Keyboard keyboard = (ref == null) ? null : ref.get();
    172         if (keyboard == null) {
    173             final KeyboardBuilder<KeyboardParams> builder =
    174                     new KeyboardBuilder<KeyboardParams>(mContext, new KeyboardParams());
    175             if (id.isAlphabetKeyboard()) {
    176                 builder.setAutoGenerate(sKeysCache);
    177             }
    178             final int keyboardXmlId = elementParams.mKeyboardXmlId;
    179             builder.load(keyboardXmlId, id);
    180             if (mParams.mDisableTouchPositionCorrectionDataForTest) {
    181                 builder.disableTouchPositionCorrectionDataForTest();
    182             }
    183             builder.setProximityCharsCorrectionEnabled(
    184                     elementParams.mProximityCharsCorrectionEnabled);
    185             keyboard = builder.build();
    186             sKeyboardCache.put(id, new SoftReference<Keyboard>(keyboard));
    187 
    188             if (DEBUG_CACHE) {
    189                 Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": "
    190                         + ((ref == null) ? "LOAD" : "GCed") + " id=" + id);
    191             }
    192         } else if (DEBUG_CACHE) {
    193             Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": HIT  id=" + id);
    194         }
    195 
    196         return keyboard;
    197     }
    198 
    199     public static final class Builder {
    200         private final Context mContext;
    201         private final String mPackageName;
    202         private final Resources mResources;
    203         private final EditorInfo mEditorInfo;
    204 
    205         private final Params mParams = new Params();
    206 
    207         private static final EditorInfo EMPTY_EDITOR_INFO = new EditorInfo();
    208 
    209         public Builder(final Context context, final EditorInfo editorInfo) {
    210             mContext = context;
    211             mPackageName = context.getPackageName();
    212             mResources = context.getResources();
    213             mEditorInfo = editorInfo;
    214             final Params params = mParams;
    215 
    216             params.mMode = getKeyboardMode(editorInfo);
    217             params.mEditorInfo = (editorInfo != null) ? editorInfo : EMPTY_EDITOR_INFO;
    218             params.mNoSettingsKey = InputAttributes.inPrivateImeOptions(
    219                     mPackageName, NO_SETTINGS_KEY, mEditorInfo);
    220         }
    221 
    222         public Builder setScreenGeometry(final int widthPixels, final int heightPixels) {
    223             final Params params = mParams;
    224             params.mOrientation = (heightPixels > widthPixels)
    225                     ? Configuration.ORIENTATION_PORTRAIT : Configuration.ORIENTATION_LANDSCAPE;
    226             setDefaultKeyboardSize(widthPixels, heightPixels);
    227             return this;
    228         }
    229 
    230         private void setDefaultKeyboardSize(final int widthPixels, final int heightPixels) {
    231             final String keyboardHeightString = ResourceUtils.getDeviceOverrideValue(
    232                     mResources, R.array.keyboard_heights);
    233             final float keyboardHeight;
    234             if (TextUtils.isEmpty(keyboardHeightString)) {
    235                 keyboardHeight = mResources.getDimension(R.dimen.keyboardHeight);
    236             } else {
    237                 keyboardHeight = Float.parseFloat(keyboardHeightString)
    238                         * mResources.getDisplayMetrics().density;
    239             }
    240             final float maxKeyboardHeight = mResources.getFraction(
    241                     R.fraction.maxKeyboardHeight, heightPixels, heightPixels);
    242             float minKeyboardHeight = mResources.getFraction(
    243                     R.fraction.minKeyboardHeight, heightPixels, heightPixels);
    244             if (minKeyboardHeight < 0.0f) {
    245                 // Specified fraction was negative, so it should be calculated against display
    246                 // width.
    247                 minKeyboardHeight = -mResources.getFraction(
    248                         R.fraction.minKeyboardHeight, widthPixels, widthPixels);
    249             }
    250             // Keyboard height will not exceed maxKeyboardHeight and will not be less than
    251             // minKeyboardHeight.
    252             mParams.mKeyboardHeight = (int)Math.max(
    253                     Math.min(keyboardHeight, maxKeyboardHeight), minKeyboardHeight);
    254             mParams.mKeyboardWidth = widthPixels;
    255         }
    256 
    257         public Builder setSubtype(final InputMethodSubtype subtype) {
    258             final boolean asciiCapable = subtype.containsExtraValueKey(ASCII_CAPABLE);
    259             @SuppressWarnings("deprecation")
    260             final boolean deprecatedForceAscii = InputAttributes.inPrivateImeOptions(
    261                     mPackageName, FORCE_ASCII, 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                     + SubtypeLocale.getKeyboardLayoutSetName(keyboardSubtype);
    271             return this;
    272         }
    273 
    274         public Builder setOptions(final boolean voiceKeyEnabled, final boolean voiceKeyOnMain,
    275                 final boolean languageSwitchKeyEnabled) {
    276             @SuppressWarnings("deprecation")
    277             final boolean deprecatedNoMicrophone = InputAttributes.inPrivateImeOptions(
    278                     null, NO_MICROPHONE_COMPAT, mEditorInfo);
    279             final boolean noMicrophone = InputAttributes.inPrivateImeOptions(
    280                     mPackageName, NO_MICROPHONE, mEditorInfo)
    281                     || deprecatedNoMicrophone;
    282             mParams.mVoiceKeyEnabled = voiceKeyEnabled && !noMicrophone;
    283             mParams.mVoiceKeyOnMain = voiceKeyOnMain;
    284             mParams.mLanguageSwitchKeyEnabled = languageSwitchKeyEnabled;
    285             return this;
    286         }
    287 
    288         public void disableTouchPositionCorrectionData() {
    289             mParams.mDisableTouchPositionCorrectionDataForTest = true;
    290         }
    291 
    292         public KeyboardLayoutSet build() {
    293             if (mParams.mOrientation == Configuration.ORIENTATION_UNDEFINED)
    294                 throw new RuntimeException("Screen geometry is not specified");
    295             if (mParams.mSubtype == null)
    296                 throw new RuntimeException("KeyboardLayoutSet subtype is not specified");
    297             final String packageName = mResources.getResourcePackageName(
    298                     R.xml.keyboard_layout_set_qwerty);
    299             final String keyboardLayoutSetName = mParams.mKeyboardLayoutSetName;
    300             final int xmlId = mResources.getIdentifier(keyboardLayoutSetName, "xml", packageName);
    301             try {
    302                 parseKeyboardLayoutSet(mResources, xmlId);
    303             } catch (final IOException e) {
    304                 throw new RuntimeException(e.getMessage() + " in " + keyboardLayoutSetName, e);
    305             } catch (final XmlPullParserException e) {
    306                 throw new RuntimeException(e.getMessage() + " in " + keyboardLayoutSetName, e);
    307             }
    308             return new KeyboardLayoutSet(mContext, mParams);
    309         }
    310 
    311         private void parseKeyboardLayoutSet(final Resources res, final int resId)
    312                 throws XmlPullParserException, IOException {
    313             final XmlResourceParser parser = res.getXml(resId);
    314             try {
    315                 while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
    316                     final int event = parser.next();
    317                     if (event == XmlPullParser.START_TAG) {
    318                         final String tag = parser.getName();
    319                         if (TAG_KEYBOARD_SET.equals(tag)) {
    320                             parseKeyboardLayoutSetContent(parser);
    321                         } else {
    322                             throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD_SET);
    323                         }
    324                     }
    325                 }
    326             } finally {
    327                 parser.close();
    328             }
    329         }
    330 
    331         private void parseKeyboardLayoutSetContent(final XmlPullParser parser)
    332                 throws XmlPullParserException, IOException {
    333             while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
    334                 final int event = parser.next();
    335                 if (event == XmlPullParser.START_TAG) {
    336                     final String tag = parser.getName();
    337                     if (TAG_ELEMENT.equals(tag)) {
    338                         parseKeyboardLayoutSetElement(parser);
    339                     } else {
    340                         throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD_SET);
    341                     }
    342                 } else if (event == XmlPullParser.END_TAG) {
    343                     final String tag = parser.getName();
    344                     if (TAG_KEYBOARD_SET.equals(tag)) {
    345                         break;
    346                     } else {
    347                         throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_KEYBOARD_SET);
    348                     }
    349                 }
    350             }
    351         }
    352 
    353         private void parseKeyboardLayoutSetElement(final XmlPullParser parser)
    354                 throws XmlPullParserException, IOException {
    355             final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
    356                     R.styleable.KeyboardLayoutSet_Element);
    357             try {
    358                 XmlParseUtils.checkAttributeExists(a,
    359                         R.styleable.KeyboardLayoutSet_Element_elementName, "elementName",
    360                         TAG_ELEMENT, parser);
    361                 XmlParseUtils.checkAttributeExists(a,
    362                         R.styleable.KeyboardLayoutSet_Element_elementKeyboard, "elementKeyboard",
    363                         TAG_ELEMENT, parser);
    364                 XmlParseUtils.checkEndTag(TAG_ELEMENT, parser);
    365 
    366                 final ElementParams elementParams = new ElementParams();
    367                 final int elementName = a.getInt(
    368                         R.styleable.KeyboardLayoutSet_Element_elementName, 0);
    369                 elementParams.mKeyboardXmlId = a.getResourceId(
    370                         R.styleable.KeyboardLayoutSet_Element_elementKeyboard, 0);
    371                 elementParams.mProximityCharsCorrectionEnabled = a.getBoolean(
    372                         R.styleable.KeyboardLayoutSet_Element_enableProximityCharsCorrection,
    373                         false);
    374                 mParams.mKeyboardLayoutSetElementIdToParamsMap.put(elementName, elementParams);
    375             } finally {
    376                 a.recycle();
    377             }
    378         }
    379 
    380         private static int getKeyboardMode(final EditorInfo editorInfo) {
    381             if (editorInfo == null)
    382                 return KeyboardId.MODE_TEXT;
    383 
    384             final int inputType = editorInfo.inputType;
    385             final int variation = inputType & InputType.TYPE_MASK_VARIATION;
    386 
    387             switch (inputType & InputType.TYPE_MASK_CLASS) {
    388             case InputType.TYPE_CLASS_NUMBER:
    389                 return KeyboardId.MODE_NUMBER;
    390             case InputType.TYPE_CLASS_DATETIME:
    391                 switch (variation) {
    392                 case InputType.TYPE_DATETIME_VARIATION_DATE:
    393                     return KeyboardId.MODE_DATE;
    394                 case InputType.TYPE_DATETIME_VARIATION_TIME:
    395                     return KeyboardId.MODE_TIME;
    396                 default: // InputType.TYPE_DATETIME_VARIATION_NORMAL
    397                     return KeyboardId.MODE_DATETIME;
    398                 }
    399             case InputType.TYPE_CLASS_PHONE:
    400                 return KeyboardId.MODE_PHONE;
    401             case InputType.TYPE_CLASS_TEXT:
    402                 if (InputTypeUtils.isEmailVariation(variation)) {
    403                     return KeyboardId.MODE_EMAIL;
    404                 } else if (variation == InputType.TYPE_TEXT_VARIATION_URI) {
    405                     return KeyboardId.MODE_URL;
    406                 } else if (variation == InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE) {
    407                     return KeyboardId.MODE_IM;
    408                 } else if (variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
    409                     return KeyboardId.MODE_TEXT;
    410                 } else {
    411                     return KeyboardId.MODE_TEXT;
    412                 }
    413             default:
    414                 return KeyboardId.MODE_TEXT;
    415             }
    416         }
    417     }
    418 
    419     public static KeyboardLayoutSet createKeyboardSetForSpellChecker(final Context context,
    420             final String locale, final String layout) {
    421         final InputMethodSubtype subtype =
    422                 AdditionalSubtype.createAdditionalSubtype(locale, layout, null);
    423         return createKeyboardSet(context, subtype, SPELLCHECKER_DUMMY_KEYBOARD_WIDTH,
    424                 SPELLCHECKER_DUMMY_KEYBOARD_HEIGHT, false);
    425     }
    426 
    427     public static KeyboardLayoutSet createKeyboardSetForTest(final Context context,
    428             final InputMethodSubtype subtype, final int orientation,
    429             final boolean testCasesHaveTouchCoordinates) {
    430         final DisplayMetrics dm = context.getResources().getDisplayMetrics();
    431         final int width;
    432         final int height;
    433         if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
    434             width = Math.max(dm.widthPixels, dm.heightPixels);
    435             height = Math.min(dm.widthPixels, dm.heightPixels);
    436         } else if (orientation == Configuration.ORIENTATION_PORTRAIT) {
    437             width = Math.min(dm.widthPixels, dm.heightPixels);
    438             height = Math.max(dm.widthPixels, dm.heightPixels);
    439         } else {
    440             throw new RuntimeException("Orientation should be ORIENTATION_LANDSCAPE or "
    441                     + "ORIENTATION_PORTRAIT: orientation=" + orientation);
    442         }
    443         return createKeyboardSet(context, subtype, width, height, testCasesHaveTouchCoordinates);
    444     }
    445 
    446     private static KeyboardLayoutSet createKeyboardSet(final Context context,
    447             final InputMethodSubtype subtype, final int width, final int height,
    448             final boolean testCasesHaveTouchCoordinates) {
    449         final EditorInfo editorInfo = new EditorInfo();
    450         editorInfo.inputType = InputType.TYPE_CLASS_TEXT;
    451         final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder(
    452                 context, editorInfo);
    453         builder.setScreenGeometry(width, height);
    454         builder.setSubtype(subtype);
    455         if (!testCasesHaveTouchCoordinates) {
    456             // For spell checker and tests
    457             builder.disableTouchPositionCorrectionData();
    458         }
    459         return builder.build();
    460     }
    461 }
    462