| /* |
| * Copyright (C) 2011 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.inputmethod.keyboard; |
| |
| import static com.android.inputmethod.latin.Constants.ImeOption.FORCE_ASCII; |
| import static com.android.inputmethod.latin.Constants.ImeOption.NO_MICROPHONE; |
| import static com.android.inputmethod.latin.Constants.ImeOption.NO_MICROPHONE_COMPAT; |
| import static com.android.inputmethod.latin.Constants.ImeOption.NO_SETTINGS_KEY; |
| import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.ASCII_CAPABLE; |
| |
| import android.content.Context; |
| import android.content.res.Configuration; |
| import android.content.res.Resources; |
| import android.content.res.TypedArray; |
| import android.content.res.XmlResourceParser; |
| import android.text.InputType; |
| import android.text.TextUtils; |
| import android.util.Log; |
| import android.util.SparseArray; |
| import android.util.Xml; |
| import android.view.inputmethod.EditorInfo; |
| import android.view.inputmethod.InputMethodSubtype; |
| |
| import com.android.inputmethod.annotations.UsedForTesting; |
| import com.android.inputmethod.compat.EditorInfoCompatUtils; |
| import com.android.inputmethod.keyboard.internal.KeyboardBuilder; |
| import com.android.inputmethod.keyboard.internal.KeyboardParams; |
| import com.android.inputmethod.keyboard.internal.KeysCache; |
| import com.android.inputmethod.latin.CollectionUtils; |
| import com.android.inputmethod.latin.InputAttributes; |
| import com.android.inputmethod.latin.InputTypeUtils; |
| import com.android.inputmethod.latin.LatinImeLogger; |
| import com.android.inputmethod.latin.R; |
| import com.android.inputmethod.latin.ResourceUtils; |
| import com.android.inputmethod.latin.SubtypeLocale; |
| import com.android.inputmethod.latin.SubtypeSwitcher; |
| import com.android.inputmethod.latin.XmlParseUtils; |
| |
| import org.xmlpull.v1.XmlPullParser; |
| import org.xmlpull.v1.XmlPullParserException; |
| |
| import java.io.IOException; |
| import java.lang.ref.SoftReference; |
| import java.util.HashMap; |
| |
| /** |
| * This class represents a set of keyboard layouts. Each of them represents a different keyboard |
| * specific to a keyboard state, such as alphabet, symbols, and so on. Layouts in the same |
| * {@link KeyboardLayoutSet} are related to each other. |
| * A {@link KeyboardLayoutSet} needs to be created for each |
| * {@link android.view.inputmethod.EditorInfo}. |
| */ |
| public final class KeyboardLayoutSet { |
| private static final String TAG = KeyboardLayoutSet.class.getSimpleName(); |
| private static final boolean DEBUG_CACHE = LatinImeLogger.sDBG; |
| |
| private static final String TAG_KEYBOARD_SET = "KeyboardLayoutSet"; |
| private static final String TAG_ELEMENT = "Element"; |
| |
| private static final String KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX = "keyboard_layout_set_"; |
| |
| private final Context mContext; |
| private final Params mParams; |
| |
| private static final HashMap<KeyboardId, SoftReference<Keyboard>> sKeyboardCache = |
| CollectionUtils.newHashMap(); |
| private static final KeysCache sKeysCache = new KeysCache(); |
| |
| @SuppressWarnings("serial") |
| public static final class KeyboardLayoutSetException extends RuntimeException { |
| public final KeyboardId mKeyboardId; |
| |
| public KeyboardLayoutSetException(final Throwable cause, final KeyboardId keyboardId) { |
| super(cause); |
| mKeyboardId = keyboardId; |
| } |
| } |
| |
| private static final class ElementParams { |
| int mKeyboardXmlId; |
| boolean mProximityCharsCorrectionEnabled; |
| public ElementParams() {} |
| } |
| |
| public static final class Params { |
| String mKeyboardLayoutSetName; |
| int mMode; |
| EditorInfo mEditorInfo; |
| boolean mDisableTouchPositionCorrectionDataForTest; |
| boolean mVoiceKeyEnabled; |
| boolean mVoiceKeyOnMain; |
| boolean mNoSettingsKey; |
| boolean mLanguageSwitchKeyEnabled; |
| InputMethodSubtype mSubtype; |
| int mDeviceFormFactor; |
| int mOrientation; |
| int mKeyboardWidth; |
| int mKeyboardHeight; |
| // Sparse array of KeyboardLayoutSet element parameters indexed by element's id. |
| final SparseArray<ElementParams> mKeyboardLayoutSetElementIdToParamsMap = |
| CollectionUtils.newSparseArray(); |
| } |
| |
| public static void clearKeyboardCache() { |
| sKeyboardCache.clear(); |
| sKeysCache.clear(); |
| } |
| |
| KeyboardLayoutSet(final Context context, final Params params) { |
| mContext = context; |
| mParams = params; |
| } |
| |
| public Keyboard getKeyboard(final int baseKeyboardLayoutSetElementId) { |
| final int keyboardLayoutSetElementId; |
| switch (mParams.mMode) { |
| case KeyboardId.MODE_PHONE: |
| if (baseKeyboardLayoutSetElementId == KeyboardId.ELEMENT_SYMBOLS) { |
| keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE_SYMBOLS; |
| } else { |
| keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE; |
| } |
| break; |
| case KeyboardId.MODE_NUMBER: |
| case KeyboardId.MODE_DATE: |
| case KeyboardId.MODE_TIME: |
| case KeyboardId.MODE_DATETIME: |
| keyboardLayoutSetElementId = KeyboardId.ELEMENT_NUMBER; |
| break; |
| default: |
| keyboardLayoutSetElementId = baseKeyboardLayoutSetElementId; |
| break; |
| } |
| |
| ElementParams elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get( |
| keyboardLayoutSetElementId); |
| if (elementParams == null) { |
| elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get( |
| KeyboardId.ELEMENT_ALPHABET); |
| } |
| // Note: The keyboard for each shift state, and mode are represented as an elementName |
| // attribute in a keyboard_layout_set XML file. Also each keyboard layout XML resource is |
| // specified as an elementKeyboard attribute in the file. |
| // The KeyboardId is an internal key for a Keyboard object. |
| final KeyboardId id = new KeyboardId(keyboardLayoutSetElementId, mParams); |
| try { |
| return getKeyboard(elementParams, id); |
| } catch (RuntimeException e) { |
| throw new KeyboardLayoutSetException(e, id); |
| } |
| } |
| |
| private Keyboard getKeyboard(final ElementParams elementParams, final KeyboardId id) { |
| final SoftReference<Keyboard> ref = sKeyboardCache.get(id); |
| Keyboard keyboard = (ref == null) ? null : ref.get(); |
| if (keyboard == null) { |
| final KeyboardBuilder<KeyboardParams> builder = |
| new KeyboardBuilder<KeyboardParams>(mContext, new KeyboardParams()); |
| if (id.isAlphabetKeyboard()) { |
| builder.setAutoGenerate(sKeysCache); |
| } |
| final int keyboardXmlId = elementParams.mKeyboardXmlId; |
| builder.load(keyboardXmlId, id); |
| if (mParams.mDisableTouchPositionCorrectionDataForTest) { |
| builder.disableTouchPositionCorrectionDataForTest(); |
| } |
| builder.setProximityCharsCorrectionEnabled( |
| elementParams.mProximityCharsCorrectionEnabled); |
| keyboard = builder.build(); |
| sKeyboardCache.put(id, new SoftReference<Keyboard>(keyboard)); |
| |
| if (DEBUG_CACHE) { |
| Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": " |
| + ((ref == null) ? "LOAD" : "GCed") + " id=" + id); |
| } |
| } else if (DEBUG_CACHE) { |
| Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": HIT id=" + id); |
| } |
| |
| return keyboard; |
| } |
| |
| public static final class Builder { |
| private final Context mContext; |
| private final String mPackageName; |
| private final Resources mResources; |
| private final EditorInfo mEditorInfo; |
| |
| private final Params mParams = new Params(); |
| |
| private static final EditorInfo EMPTY_EDITOR_INFO = new EditorInfo(); |
| |
| public Builder(final Context context, final EditorInfo editorInfo) { |
| mContext = context; |
| mPackageName = context.getPackageName(); |
| mResources = context.getResources(); |
| mEditorInfo = editorInfo; |
| final Params params = mParams; |
| |
| params.mMode = getKeyboardMode(editorInfo); |
| params.mEditorInfo = (editorInfo != null) ? editorInfo : EMPTY_EDITOR_INFO; |
| params.mNoSettingsKey = InputAttributes.inPrivateImeOptions( |
| mPackageName, NO_SETTINGS_KEY, mEditorInfo); |
| } |
| |
| public Builder setScreenGeometry(final int deviceFormFactor, final int widthPixels, |
| final int heightPixels) { |
| final Params params = mParams; |
| params.mDeviceFormFactor = deviceFormFactor; |
| params.mOrientation = (heightPixels > widthPixels) |
| ? Configuration.ORIENTATION_PORTRAIT : Configuration.ORIENTATION_LANDSCAPE; |
| setDefaultKeyboardSize(widthPixels, heightPixels); |
| return this; |
| } |
| |
| private void setDefaultKeyboardSize(final int widthPixels, final int heightPixels) { |
| final String keyboardHeightString = ResourceUtils.getDeviceOverrideValue( |
| mResources, R.array.keyboard_heights); |
| final float keyboardHeight; |
| if (TextUtils.isEmpty(keyboardHeightString)) { |
| keyboardHeight = mResources.getDimension(R.dimen.keyboardHeight); |
| } else { |
| keyboardHeight = Float.parseFloat(keyboardHeightString) |
| * mResources.getDisplayMetrics().density; |
| } |
| final float maxKeyboardHeight = mResources.getFraction( |
| R.fraction.maxKeyboardHeight, heightPixels, heightPixels); |
| float minKeyboardHeight = mResources.getFraction( |
| R.fraction.minKeyboardHeight, heightPixels, heightPixels); |
| if (minKeyboardHeight < 0.0f) { |
| // Specified fraction was negative, so it should be calculated against display |
| // width. |
| minKeyboardHeight = -mResources.getFraction( |
| R.fraction.minKeyboardHeight, widthPixels, widthPixels); |
| } |
| // Keyboard height will not exceed maxKeyboardHeight and will not be less than |
| // minKeyboardHeight. |
| mParams.mKeyboardHeight = (int)Math.max( |
| Math.min(keyboardHeight, maxKeyboardHeight), minKeyboardHeight); |
| mParams.mKeyboardWidth = widthPixels; |
| } |
| |
| public Builder setSubtype(final InputMethodSubtype subtype) { |
| final boolean asciiCapable = subtype.containsExtraValueKey(ASCII_CAPABLE); |
| @SuppressWarnings("deprecation") |
| final boolean deprecatedForceAscii = InputAttributes.inPrivateImeOptions( |
| mPackageName, FORCE_ASCII, mEditorInfo); |
| final boolean forceAscii = EditorInfoCompatUtils.hasFlagForceAscii( |
| mParams.mEditorInfo.imeOptions) |
| || deprecatedForceAscii; |
| final InputMethodSubtype keyboardSubtype = (forceAscii && !asciiCapable) |
| ? SubtypeSwitcher.getInstance().getNoLanguageSubtype() |
| : subtype; |
| mParams.mSubtype = keyboardSubtype; |
| mParams.mKeyboardLayoutSetName = KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX |
| + SubtypeLocale.getKeyboardLayoutSetName(keyboardSubtype); |
| return this; |
| } |
| |
| public Builder setOptions(final boolean voiceKeyEnabled, final boolean voiceKeyOnMain, |
| final boolean languageSwitchKeyEnabled) { |
| @SuppressWarnings("deprecation") |
| final boolean deprecatedNoMicrophone = InputAttributes.inPrivateImeOptions( |
| null, NO_MICROPHONE_COMPAT, mEditorInfo); |
| final boolean noMicrophone = InputAttributes.inPrivateImeOptions( |
| mPackageName, NO_MICROPHONE, mEditorInfo) |
| || deprecatedNoMicrophone; |
| mParams.mVoiceKeyEnabled = voiceKeyEnabled && !noMicrophone; |
| mParams.mVoiceKeyOnMain = voiceKeyOnMain; |
| mParams.mLanguageSwitchKeyEnabled = languageSwitchKeyEnabled; |
| return this; |
| } |
| |
| @UsedForTesting |
| public void disableTouchPositionCorrectionDataForTest() { |
| mParams.mDisableTouchPositionCorrectionDataForTest = true; |
| } |
| |
| public KeyboardLayoutSet build() { |
| if (mParams.mOrientation == Configuration.ORIENTATION_UNDEFINED) |
| throw new RuntimeException("Screen geometry is not specified"); |
| if (mParams.mSubtype == null) |
| throw new RuntimeException("KeyboardLayoutSet subtype is not specified"); |
| final String packageName = mResources.getResourcePackageName( |
| R.xml.keyboard_layout_set_qwerty); |
| final String keyboardLayoutSetName = mParams.mKeyboardLayoutSetName; |
| final int xmlId = mResources.getIdentifier(keyboardLayoutSetName, "xml", packageName); |
| try { |
| parseKeyboardLayoutSet(mResources, xmlId); |
| } catch (final IOException e) { |
| throw new RuntimeException(e.getMessage() + " in " + keyboardLayoutSetName, e); |
| } catch (final XmlPullParserException e) { |
| throw new RuntimeException(e.getMessage() + " in " + keyboardLayoutSetName, e); |
| } |
| return new KeyboardLayoutSet(mContext, mParams); |
| } |
| |
| private void parseKeyboardLayoutSet(final Resources res, final int resId) |
| throws XmlPullParserException, IOException { |
| final XmlResourceParser parser = res.getXml(resId); |
| try { |
| while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { |
| final int event = parser.next(); |
| if (event == XmlPullParser.START_TAG) { |
| final String tag = parser.getName(); |
| if (TAG_KEYBOARD_SET.equals(tag)) { |
| parseKeyboardLayoutSetContent(parser); |
| } else { |
| throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD_SET); |
| } |
| } |
| } |
| } finally { |
| parser.close(); |
| } |
| } |
| |
| private void parseKeyboardLayoutSetContent(final XmlPullParser parser) |
| throws XmlPullParserException, IOException { |
| while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { |
| final int event = parser.next(); |
| if (event == XmlPullParser.START_TAG) { |
| final String tag = parser.getName(); |
| if (TAG_ELEMENT.equals(tag)) { |
| parseKeyboardLayoutSetElement(parser); |
| } else { |
| throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD_SET); |
| } |
| } else if (event == XmlPullParser.END_TAG) { |
| final String tag = parser.getName(); |
| if (TAG_KEYBOARD_SET.equals(tag)) { |
| break; |
| } else { |
| throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_KEYBOARD_SET); |
| } |
| } |
| } |
| } |
| |
| private void parseKeyboardLayoutSetElement(final XmlPullParser parser) |
| throws XmlPullParserException, IOException { |
| final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser), |
| R.styleable.KeyboardLayoutSet_Element); |
| try { |
| XmlParseUtils.checkAttributeExists(a, |
| R.styleable.KeyboardLayoutSet_Element_elementName, "elementName", |
| TAG_ELEMENT, parser); |
| XmlParseUtils.checkAttributeExists(a, |
| R.styleable.KeyboardLayoutSet_Element_elementKeyboard, "elementKeyboard", |
| TAG_ELEMENT, parser); |
| XmlParseUtils.checkEndTag(TAG_ELEMENT, parser); |
| |
| final ElementParams elementParams = new ElementParams(); |
| final int elementName = a.getInt( |
| R.styleable.KeyboardLayoutSet_Element_elementName, 0); |
| elementParams.mKeyboardXmlId = a.getResourceId( |
| R.styleable.KeyboardLayoutSet_Element_elementKeyboard, 0); |
| elementParams.mProximityCharsCorrectionEnabled = a.getBoolean( |
| R.styleable.KeyboardLayoutSet_Element_enableProximityCharsCorrection, |
| false); |
| mParams.mKeyboardLayoutSetElementIdToParamsMap.put(elementName, elementParams); |
| } finally { |
| a.recycle(); |
| } |
| } |
| |
| private static int getKeyboardMode(final EditorInfo editorInfo) { |
| if (editorInfo == null) |
| return KeyboardId.MODE_TEXT; |
| |
| final int inputType = editorInfo.inputType; |
| final int variation = inputType & InputType.TYPE_MASK_VARIATION; |
| |
| switch (inputType & InputType.TYPE_MASK_CLASS) { |
| case InputType.TYPE_CLASS_NUMBER: |
| return KeyboardId.MODE_NUMBER; |
| case InputType.TYPE_CLASS_DATETIME: |
| switch (variation) { |
| case InputType.TYPE_DATETIME_VARIATION_DATE: |
| return KeyboardId.MODE_DATE; |
| case InputType.TYPE_DATETIME_VARIATION_TIME: |
| return KeyboardId.MODE_TIME; |
| default: // InputType.TYPE_DATETIME_VARIATION_NORMAL |
| return KeyboardId.MODE_DATETIME; |
| } |
| case InputType.TYPE_CLASS_PHONE: |
| return KeyboardId.MODE_PHONE; |
| case InputType.TYPE_CLASS_TEXT: |
| if (InputTypeUtils.isEmailVariation(variation)) { |
| return KeyboardId.MODE_EMAIL; |
| } else if (variation == InputType.TYPE_TEXT_VARIATION_URI) { |
| return KeyboardId.MODE_URL; |
| } else if (variation == InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE) { |
| return KeyboardId.MODE_IM; |
| } else if (variation == InputType.TYPE_TEXT_VARIATION_FILTER) { |
| return KeyboardId.MODE_TEXT; |
| } else { |
| return KeyboardId.MODE_TEXT; |
| } |
| default: |
| return KeyboardId.MODE_TEXT; |
| } |
| } |
| } |
| } |