blob: 1c66c37d336bcb375d1c054c6122891efba42c7f [file] [log] [blame]
/*
* 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_SETTINGS_KEY;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
import android.text.InputType;
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.compat.EditorInfoCompatUtils;
import com.android.inputmethod.compat.InputMethodSubtypeCompatUtils;
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.InputAttributes;
import com.android.inputmethod.latin.R;
import com.android.inputmethod.latin.RichInputMethodSubtype;
import com.android.inputmethod.latin.SubtypeSwitcher;
import com.android.inputmethod.latin.define.DebugFlags;
import com.android.inputmethod.latin.utils.DebugLogUtils;
import com.android.inputmethod.latin.utils.InputTypeUtils;
import com.android.inputmethod.latin.utils.ScriptUtils;
import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
import com.android.inputmethod.latin.utils.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 = DebugFlags.DEBUG_ENABLED;
private static final String TAG_KEYBOARD_SET = "KeyboardLayoutSet";
private static final String TAG_ELEMENT = "Element";
private static final String TAG_FEATURE = "Feature";
private static final String KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX = "keyboard_layout_set_";
private final Context mContext;
private final Params mParams;
// How many layouts we forcibly keep in cache. This only includes ALPHABET (default) and
// ALPHABET_AUTOMATIC_SHIFTED layouts - other layouts may stay in memory in the map of
// soft-references, but we forcibly cache this many alphabetic/auto-shifted layouts.
private static final int FORCIBLE_CACHE_SIZE = 4;
// By construction of soft references, anything that is also referenced somewhere else
// will stay in the cache. So we forcibly keep some references in an array to prevent
// them from disappearing from sKeyboardCache.
private static final Keyboard[] sForcibleKeyboardCache = new Keyboard[FORCIBLE_CACHE_SIZE];
private static final HashMap<KeyboardId, SoftReference<Keyboard>> sKeyboardCache =
new HashMap<>();
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;
boolean mSupportsSplitLayout;
boolean mAllowRedundantMoreKeys;
public ElementParams() {}
}
public static final class Params {
String mKeyboardLayoutSetName;
int mMode;
boolean mDisableTouchPositionCorrectionDataForTest;
// TODO: Use {@link InputAttributes} instead of these variables.
EditorInfo mEditorInfo;
boolean mIsPasswordField;
boolean mVoiceInputKeyEnabled;
boolean mNoSettingsKey;
boolean mLanguageSwitchKeyEnabled;
RichInputMethodSubtype mSubtype;
boolean mIsSpellChecker;
int mKeyboardWidth;
int mKeyboardHeight;
int mScriptId = ScriptUtils.SCRIPT_LATIN;
// Indicates if the user has enabled the split-layout preference
// and the required ProductionFlags are enabled.
boolean mIsSplitLayoutEnabledByUser;
// Indicates if split layout is actually enabled, taking into account
// whether the user has enabled it, and the keyboard layout supports it.
boolean mIsSplitLayoutEnabled;
// Sparse array of KeyboardLayoutSet element parameters indexed by element's id.
final SparseArray<ElementParams> mKeyboardLayoutSetElementIdToParamsMap =
new SparseArray<>();
}
public static void onSystemLocaleChanged() {
clearKeyboardCache();
}
public static void onKeyboardThemeChanged() {
clearKeyboardCache();
}
private 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.
mParams.mIsSplitLayoutEnabled = mParams.mIsSplitLayoutEnabledByUser
&& elementParams.mSupportsSplitLayout;
final KeyboardId id = new KeyboardId(keyboardLayoutSetElementId, mParams);
try {
return getKeyboard(elementParams, id);
} catch (final RuntimeException e) {
Log.e(TAG, "Can't create keyboard: " + id, e);
throw new KeyboardLayoutSetException(e, id);
}
}
private Keyboard getKeyboard(final ElementParams elementParams, final KeyboardId id) {
final SoftReference<Keyboard> ref = sKeyboardCache.get(id);
final Keyboard cachedKeyboard = (ref == null) ? null : ref.get();
if (cachedKeyboard != null) {
if (DEBUG_CACHE) {
Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": HIT id=" + id);
}
return cachedKeyboard;
}
final KeyboardBuilder<KeyboardParams> builder =
new KeyboardBuilder<>(mContext, new KeyboardParams());
if (id.isAlphabetKeyboard()) {
builder.setAutoGenerate(sKeysCache);
}
builder.setAllowRedundantMoreKes(elementParams.mAllowRedundantMoreKeys);
final int keyboardXmlId = elementParams.mKeyboardXmlId;
builder.load(keyboardXmlId, id);
if (mParams.mDisableTouchPositionCorrectionDataForTest) {
builder.disableTouchPositionCorrectionDataForTest();
}
builder.setProximityCharsCorrectionEnabled(elementParams.mProximityCharsCorrectionEnabled);
final Keyboard keyboard = builder.build();
sKeyboardCache.put(id, new SoftReference<>(keyboard));
if ((id.mElementId == KeyboardId.ELEMENT_ALPHABET
|| id.mElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED)
&& !mParams.mIsSpellChecker) {
// We only forcibly cache the primary, "ALPHABET", layouts.
for (int i = sForcibleKeyboardCache.length - 1; i >= 1; --i) {
sForcibleKeyboardCache[i] = sForcibleKeyboardCache[i - 1];
}
sForcibleKeyboardCache[0] = keyboard;
if (DEBUG_CACHE) {
Log.d(TAG, "forcing caching of keyboard with id=" + id);
}
}
if (DEBUG_CACHE) {
Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": "
+ ((ref == null) ? "LOAD" : "GCed") + " id=" + id);
}
return keyboard;
}
public int getScriptId() {
return mParams.mScriptId;
}
public static final class Builder {
private final Context mContext;
private final String mPackageName;
private final Resources mResources;
private final Params mParams = new Params();
private static final EditorInfo EMPTY_EDITOR_INFO = new EditorInfo();
public Builder(final Context context, final EditorInfo ei) {
mContext = context;
mPackageName = context.getPackageName();
mResources = context.getResources();
final Params params = mParams;
final EditorInfo editorInfo = (ei != null) ? ei : EMPTY_EDITOR_INFO;
params.mMode = getKeyboardMode(editorInfo);
// TODO: Consolidate those with {@link InputAttributes}.
params.mEditorInfo = editorInfo;
params.mIsPasswordField = InputTypeUtils.isPasswordInputType(editorInfo.inputType);
params.mNoSettingsKey = InputAttributes.inPrivateImeOptions(
mPackageName, NO_SETTINGS_KEY, editorInfo);
}
public Builder setKeyboardGeometry(final int keyboardWidth, final int keyboardHeight) {
mParams.mKeyboardWidth = keyboardWidth;
mParams.mKeyboardHeight = keyboardHeight;
return this;
}
public Builder setSubtype(final RichInputMethodSubtype subtype) {
final boolean asciiCapable = InputMethodSubtypeCompatUtils.isAsciiCapable(subtype);
// TODO: Consolidate with {@link InputAttributes}.
@SuppressWarnings("deprecation")
final boolean deprecatedForceAscii = InputAttributes.inPrivateImeOptions(
mPackageName, FORCE_ASCII, mParams.mEditorInfo);
final boolean forceAscii = EditorInfoCompatUtils.hasFlagForceAscii(
mParams.mEditorInfo.imeOptions)
|| deprecatedForceAscii;
final RichInputMethodSubtype keyboardSubtype = (forceAscii && !asciiCapable)
? SubtypeSwitcher.getInstance().getNoLanguageSubtype()
: subtype;
mParams.mSubtype = keyboardSubtype;
mParams.mKeyboardLayoutSetName = KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX
+ SubtypeLocaleUtils.getKeyboardLayoutSetName(keyboardSubtype);
return this;
}
public Builder setIsSpellChecker(final boolean isSpellChecker) {
mParams.mIsSpellChecker = isSpellChecker;
return this;
}
public Builder setVoiceInputKeyEnabled(final boolean enabled) {
mParams.mVoiceInputKeyEnabled = enabled;
return this;
}
public Builder setLanguageSwitchKeyEnabled(final boolean enabled) {
mParams.mLanguageSwitchKeyEnabled = enabled;
return this;
}
public Builder disableTouchPositionCorrectionData() {
mParams.mDisableTouchPositionCorrectionDataForTest = true;
return this;
}
public Builder setScriptId(final int scriptId) {
mParams.mScriptId = scriptId;
return this;
}
public Builder setSplitLayoutEnabledByUser(final boolean enabled) {
mParams.mIsSplitLayoutEnabledByUser = enabled;
return this;
}
private final static HashMap<InputMethodSubtype, Integer> sScriptIdsForSubtypes =
new HashMap<>();
public static int getScriptId(final Resources resources, final InputMethodSubtype subtype) {
final Integer value = sScriptIdsForSubtypes.get(subtype);
if (null == value) {
final int scriptId = readScriptId(resources, subtype);
sScriptIdsForSubtypes.put(subtype, scriptId);
return scriptId;
}
return value;
}
// Super redux version of reading the script ID for some subtype from Xml.
private static int readScriptId(final Resources resources,
final InputMethodSubtype subtype) {
final String layoutSetName = KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX
+ SubtypeLocaleUtils.getKeyboardLayoutSetName(subtype);
final int xmlId = getXmlId(resources, layoutSetName);
final XmlResourceParser parser = resources.getXml(xmlId);
try {
while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
// Bovinate through the XML stupidly searching for TAG_FEATURE, and read
// the script Id from it.
parser.next();
final String tag = parser.getName();
if (TAG_FEATURE.equals(tag)) {
return readScriptIdFromTagFeature(resources, parser);
}
}
} catch (final IOException | XmlPullParserException e) {
throw new RuntimeException(e.getMessage() + " in " + layoutSetName, e);
} finally {
parser.close();
}
// If the tag is not found, then the default script is Latin.
return ScriptUtils.SCRIPT_LATIN;
}
private static int readScriptIdFromTagFeature(final Resources resources,
final XmlPullParser parser) throws IOException, XmlPullParserException {
final TypedArray featureAttr = resources.obtainAttributes(Xml.asAttributeSet(parser),
R.styleable.KeyboardLayoutSet_Feature);
try {
final int scriptId =
featureAttr.getInt(R.styleable.KeyboardLayoutSet_Feature_supportedScript,
ScriptUtils.SCRIPT_UNKNOWN);
XmlParseUtils.checkEndTag(TAG_FEATURE, parser);
return scriptId;
} finally {
featureAttr.recycle();
}
}
public KeyboardLayoutSet build() {
if (mParams.mSubtype == null)
throw new RuntimeException("KeyboardLayoutSet subtype is not specified");
final int xmlId = getXmlId(mResources, mParams.mKeyboardLayoutSetName);
try {
parseKeyboardLayoutSet(mResources, xmlId);
} catch (final IOException | XmlPullParserException e) {
throw new RuntimeException(e.getMessage() + " in " + mParams.mKeyboardLayoutSetName,
e);
}
return new KeyboardLayoutSet(mContext, mParams);
}
private static int getXmlId(final Resources resources, final String keyboardLayoutSetName) {
final String packageName = resources.getResourcePackageName(
R.xml.keyboard_layout_set_qwerty);
return resources.getIdentifier(keyboardLayoutSetName, "xml", packageName);
}
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 if (TAG_FEATURE.equals(tag)) {
parseKeyboardLayoutSetFeature(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);
elementParams.mSupportsSplitLayout = a.getBoolean(
R.styleable.KeyboardLayoutSet_Element_supportsSplitLayout, false);
elementParams.mAllowRedundantMoreKeys = a.getBoolean(
R.styleable.KeyboardLayoutSet_Element_allowRedundantMoreKeys, true);
mParams.mKeyboardLayoutSetElementIdToParamsMap.put(elementName, elementParams);
} finally {
a.recycle();
}
}
private void parseKeyboardLayoutSetFeature(final XmlPullParser parser)
throws XmlPullParserException, IOException {
final int scriptId = readScriptIdFromTagFeature(mResources, parser);
setScriptId(scriptId);
}
private static int getKeyboardMode(final EditorInfo editorInfo) {
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;
}
}
}
}