2012-08-30 05:22:40 +00:00
|
|
|
/*
|
|
|
|
* Copyright (C) 2012 The Android Open Source Project
|
|
|
|
*
|
2013-01-21 12:52:57 +00:00
|
|
|
* 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
|
2012-08-30 05:22:40 +00:00
|
|
|
*
|
2013-01-21 12:52:57 +00:00
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
2012-08-30 05:22:40 +00:00
|
|
|
*
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
2013-01-21 12:52:57 +00:00
|
|
|
* 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.
|
2012-08-30 05:22:40 +00:00
|
|
|
*/
|
|
|
|
|
|
|
|
package com.android.inputmethod.keyboard.internal;
|
|
|
|
|
2012-09-12 02:52:44 +00:00
|
|
|
import android.text.TextUtils;
|
2014-09-24 06:52:11 +00:00
|
|
|
import android.util.SparseIntArray;
|
2012-09-12 02:52:44 +00:00
|
|
|
|
2014-09-24 06:52:11 +00:00
|
|
|
import com.android.inputmethod.compat.CharacterCompat;
|
2014-02-09 14:24:02 +00:00
|
|
|
import com.android.inputmethod.keyboard.Key;
|
2012-10-29 05:46:34 +00:00
|
|
|
import com.android.inputmethod.latin.Constants;
|
2014-07-17 01:41:46 +00:00
|
|
|
import com.android.inputmethod.latin.define.DebugFlags;
|
2014-01-31 06:58:14 +00:00
|
|
|
import com.android.inputmethod.latin.utils.CollectionUtils;
|
2013-06-23 16:11:32 +00:00
|
|
|
import com.android.inputmethod.latin.utils.StringUtils;
|
2012-08-30 05:22:40 +00:00
|
|
|
|
2014-01-31 06:58:14 +00:00
|
|
|
import java.util.ArrayList;
|
|
|
|
import java.util.Arrays;
|
2014-09-24 06:52:11 +00:00
|
|
|
import java.util.HashSet;
|
2012-08-30 05:22:40 +00:00
|
|
|
import java.util.Locale;
|
|
|
|
|
2014-01-31 06:58:14 +00:00
|
|
|
/**
|
|
|
|
* The more key specification object. The more keys are an array of {@link MoreKeySpec}.
|
|
|
|
*
|
|
|
|
* The more keys specification is comma separated "key specification" each of which represents one
|
|
|
|
* "more key".
|
|
|
|
* The key specification might have label or string resource reference in it. These references are
|
|
|
|
* expanded before parsing comma.
|
|
|
|
* Special character, comma ',' backslash '\' can be escaped by '\' character.
|
|
|
|
* Note that the '\' is also parsed by XML parser and {@link MoreKeySpec#splitKeySpecs(String)}
|
|
|
|
* as well.
|
|
|
|
*/
|
2014-02-06 06:11:05 +00:00
|
|
|
// TODO: Should extend the key specification object.
|
2012-09-12 02:52:44 +00:00
|
|
|
public final class MoreKeySpec {
|
2012-08-30 05:22:40 +00:00
|
|
|
public final int mCode;
|
|
|
|
public final String mLabel;
|
|
|
|
public final String mOutputText;
|
|
|
|
public final int mIconId;
|
|
|
|
|
2014-02-10 08:48:35 +00:00
|
|
|
public MoreKeySpec(final String moreKeySpec, boolean needsToUpperCase, final Locale locale) {
|
2014-01-31 10:18:15 +00:00
|
|
|
if (TextUtils.isEmpty(moreKeySpec)) {
|
|
|
|
throw new KeySpecParser.KeySpecParserError("Empty more key spec");
|
|
|
|
}
|
2014-01-31 05:20:15 +00:00
|
|
|
mLabel = StringUtils.toUpperCaseOfStringForLocale(
|
2012-08-30 05:22:40 +00:00
|
|
|
KeySpecParser.getLabel(moreKeySpec), needsToUpperCase, locale);
|
2014-01-31 05:20:15 +00:00
|
|
|
final int code = StringUtils.toUpperCaseOfCodeForLocale(
|
2014-02-10 08:48:35 +00:00
|
|
|
KeySpecParser.getCode(moreKeySpec), needsToUpperCase, locale);
|
2012-10-29 05:46:34 +00:00
|
|
|
if (code == Constants.CODE_UNSPECIFIED) {
|
2012-08-30 05:22:40 +00:00
|
|
|
// Some letter, for example German Eszett (U+00DF: "ß"), has multiple characters
|
|
|
|
// upper case representation ("SS").
|
2012-10-29 05:46:34 +00:00
|
|
|
mCode = Constants.CODE_OUTPUT_TEXT;
|
2012-08-30 05:22:40 +00:00
|
|
|
mOutputText = mLabel;
|
|
|
|
} else {
|
|
|
|
mCode = code;
|
2014-01-31 05:20:15 +00:00
|
|
|
mOutputText = StringUtils.toUpperCaseOfStringForLocale(
|
2012-08-30 05:22:40 +00:00
|
|
|
KeySpecParser.getOutputText(moreKeySpec), needsToUpperCase, locale);
|
|
|
|
}
|
|
|
|
mIconId = KeySpecParser.getIconId(moreKeySpec);
|
|
|
|
}
|
|
|
|
|
2014-02-09 14:24:02 +00:00
|
|
|
public Key buildKey(final int x, final int y, final int labelFlags,
|
|
|
|
final KeyboardParams params) {
|
|
|
|
return new Key(mLabel, mIconId, mCode, mOutputText, null /* hintLabel */, labelFlags,
|
|
|
|
Key.BACKGROUND_TYPE_NORMAL, x, y, params.mDefaultKeyWidth, params.mDefaultRowHeight,
|
|
|
|
params.mHorizontalGap, params.mVerticalGap);
|
|
|
|
}
|
|
|
|
|
2012-09-12 02:52:44 +00:00
|
|
|
@Override
|
|
|
|
public int hashCode() {
|
|
|
|
int hashCode = 1;
|
|
|
|
hashCode = 31 + mCode;
|
|
|
|
hashCode = hashCode * 31 + mIconId;
|
|
|
|
hashCode = hashCode * 31 + (mLabel == null ? 0 : mLabel.hashCode());
|
|
|
|
hashCode = hashCode * 31 + (mOutputText == null ? 0 : mOutputText.hashCode());
|
|
|
|
return hashCode;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean equals(final Object o) {
|
|
|
|
if (this == o) return true;
|
|
|
|
if (o instanceof MoreKeySpec) {
|
|
|
|
final MoreKeySpec other = (MoreKeySpec)o;
|
|
|
|
return mCode == other.mCode
|
|
|
|
&& mIconId == other.mIconId
|
|
|
|
&& TextUtils.equals(mLabel, other.mLabel)
|
|
|
|
&& TextUtils.equals(mOutputText, other.mOutputText);
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2012-08-30 05:22:40 +00:00
|
|
|
@Override
|
|
|
|
public String toString() {
|
|
|
|
final String label = (mIconId == KeyboardIconsSet.ICON_UNDEFINED ? mLabel
|
2014-01-31 07:27:49 +00:00
|
|
|
: KeyboardIconsSet.PREFIX_ICON + KeyboardIconsSet.getIconName(mIconId));
|
2012-10-29 05:46:34 +00:00
|
|
|
final String output = (mCode == Constants.CODE_OUTPUT_TEXT ? mOutputText
|
|
|
|
: Constants.printableCode(mCode));
|
2012-08-30 05:22:40 +00:00
|
|
|
if (StringUtils.codePointCount(label) == 1 && label.codePointAt(0) == mCode) {
|
|
|
|
return output;
|
|
|
|
}
|
2014-10-20 05:48:56 +00:00
|
|
|
return label + "|" + output;
|
2012-08-30 05:22:40 +00:00
|
|
|
}
|
2014-01-31 06:58:14 +00:00
|
|
|
|
2014-09-24 06:52:11 +00:00
|
|
|
public static class LettersOnBaseLayout {
|
|
|
|
private final SparseIntArray mCodes = new SparseIntArray();
|
|
|
|
private final HashSet<String> mTexts = new HashSet<>();
|
|
|
|
|
|
|
|
public void addLetter(final Key key) {
|
|
|
|
final int code = key.getCode();
|
|
|
|
if (CharacterCompat.isAlphabetic(code)) {
|
|
|
|
mCodes.put(code, 0);
|
|
|
|
} else if (code == Constants.CODE_OUTPUT_TEXT) {
|
|
|
|
mTexts.add(key.getOutputText());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public boolean contains(final MoreKeySpec moreKey) {
|
|
|
|
final int code = moreKey.mCode;
|
|
|
|
if (CharacterCompat.isAlphabetic(code) && mCodes.indexOfKey(code) >= 0) {
|
|
|
|
return true;
|
|
|
|
} else if (code == Constants.CODE_OUTPUT_TEXT && mTexts.contains(moreKey.mOutputText)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public static MoreKeySpec[] removeRedundantMoreKeys(final MoreKeySpec[] moreKeys,
|
|
|
|
final LettersOnBaseLayout lettersOnBaseLayout) {
|
|
|
|
if (moreKeys == null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
final ArrayList<MoreKeySpec> filteredMoreKeys = new ArrayList<>();
|
|
|
|
for (final MoreKeySpec moreKey : moreKeys) {
|
|
|
|
if (!lettersOnBaseLayout.contains(moreKey)) {
|
|
|
|
filteredMoreKeys.add(moreKey);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
final int size = filteredMoreKeys.size();
|
2014-09-25 17:28:21 +00:00
|
|
|
if (size == moreKeys.length) {
|
|
|
|
return moreKeys;
|
|
|
|
}
|
|
|
|
if (size == 0) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return filteredMoreKeys.toArray(new MoreKeySpec[size]);
|
2014-09-24 06:52:11 +00:00
|
|
|
}
|
|
|
|
|
2014-07-17 01:41:46 +00:00
|
|
|
private static final boolean DEBUG = DebugFlags.DEBUG_ENABLED;
|
2014-01-31 06:58:14 +00:00
|
|
|
// Constants for parsing.
|
|
|
|
private static final char COMMA = Constants.CODE_COMMA;
|
|
|
|
private static final char BACKSLASH = Constants.CODE_BACKSLASH;
|
|
|
|
private static final String ADDITIONAL_MORE_KEY_MARKER =
|
|
|
|
StringUtils.newSingleCodePointString(Constants.CODE_PERCENT);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Split the text containing multiple key specifications separated by commas into an array of
|
|
|
|
* key specifications.
|
|
|
|
* A key specification can contain a character escaped by the backslash character, including a
|
|
|
|
* comma character.
|
|
|
|
* Note that an empty key specification will be eliminated from the result array.
|
|
|
|
*
|
|
|
|
* @param text the text containing multiple key specifications.
|
|
|
|
* @return an array of key specification text. Null if the specified <code>text</code> is empty
|
|
|
|
* or has no key specifications.
|
|
|
|
*/
|
|
|
|
public static String[] splitKeySpecs(final String text) {
|
|
|
|
if (TextUtils.isEmpty(text)) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
final int size = text.length();
|
|
|
|
// Optimization for one-letter key specification.
|
|
|
|
if (size == 1) {
|
|
|
|
return text.charAt(0) == COMMA ? null : new String[] { text };
|
|
|
|
}
|
|
|
|
|
|
|
|
ArrayList<String> list = null;
|
|
|
|
int start = 0;
|
|
|
|
// The characters in question in this loop are COMMA and BACKSLASH. These characters never
|
|
|
|
// match any high or low surrogate character. So it is OK to iterate through with char
|
|
|
|
// index.
|
|
|
|
for (int pos = 0; pos < size; pos++) {
|
|
|
|
final char c = text.charAt(pos);
|
|
|
|
if (c == COMMA) {
|
|
|
|
// Skip empty entry.
|
|
|
|
if (pos - start > 0) {
|
|
|
|
if (list == null) {
|
2014-05-23 11:18:17 +00:00
|
|
|
list = new ArrayList<>();
|
2014-01-31 06:58:14 +00:00
|
|
|
}
|
|
|
|
list.add(text.substring(start, pos));
|
|
|
|
}
|
|
|
|
// Skip comma
|
|
|
|
start = pos + 1;
|
|
|
|
} else if (c == BACKSLASH) {
|
|
|
|
// Skip escape character and escaped character.
|
|
|
|
pos++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
final String remain = (size - start > 0) ? text.substring(start) : null;
|
|
|
|
if (list == null) {
|
|
|
|
return remain != null ? new String[] { remain } : null;
|
|
|
|
}
|
|
|
|
if (remain != null) {
|
|
|
|
list.add(remain);
|
|
|
|
}
|
|
|
|
return list.toArray(new String[list.size()]);
|
|
|
|
}
|
|
|
|
|
|
|
|
private static final String[] EMPTY_STRING_ARRAY = new String[0];
|
|
|
|
|
|
|
|
private static String[] filterOutEmptyString(final String[] array) {
|
|
|
|
if (array == null) {
|
|
|
|
return EMPTY_STRING_ARRAY;
|
|
|
|
}
|
|
|
|
ArrayList<String> out = null;
|
|
|
|
for (int i = 0; i < array.length; i++) {
|
|
|
|
final String entry = array[i];
|
|
|
|
if (TextUtils.isEmpty(entry)) {
|
|
|
|
if (out == null) {
|
|
|
|
out = CollectionUtils.arrayAsList(array, 0, i);
|
|
|
|
}
|
|
|
|
} else if (out != null) {
|
|
|
|
out.add(entry);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (out == null) {
|
|
|
|
return array;
|
|
|
|
}
|
|
|
|
return out.toArray(new String[out.size()]);
|
|
|
|
}
|
|
|
|
|
|
|
|
public static String[] insertAdditionalMoreKeys(final String[] moreKeySpecs,
|
|
|
|
final String[] additionalMoreKeySpecs) {
|
|
|
|
final String[] moreKeys = filterOutEmptyString(moreKeySpecs);
|
|
|
|
final String[] additionalMoreKeys = filterOutEmptyString(additionalMoreKeySpecs);
|
|
|
|
final int moreKeysCount = moreKeys.length;
|
|
|
|
final int additionalCount = additionalMoreKeys.length;
|
|
|
|
ArrayList<String> out = null;
|
|
|
|
int additionalIndex = 0;
|
|
|
|
for (int moreKeyIndex = 0; moreKeyIndex < moreKeysCount; moreKeyIndex++) {
|
|
|
|
final String moreKeySpec = moreKeys[moreKeyIndex];
|
|
|
|
if (moreKeySpec.equals(ADDITIONAL_MORE_KEY_MARKER)) {
|
|
|
|
if (additionalIndex < additionalCount) {
|
|
|
|
// Replace '%' marker with additional more key specification.
|
|
|
|
final String additionalMoreKey = additionalMoreKeys[additionalIndex];
|
|
|
|
if (out != null) {
|
|
|
|
out.add(additionalMoreKey);
|
|
|
|
} else {
|
|
|
|
moreKeys[moreKeyIndex] = additionalMoreKey;
|
|
|
|
}
|
|
|
|
additionalIndex++;
|
|
|
|
} else {
|
|
|
|
// Filter out excessive '%' marker.
|
|
|
|
if (out == null) {
|
|
|
|
out = CollectionUtils.arrayAsList(moreKeys, 0, moreKeyIndex);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (out != null) {
|
|
|
|
out.add(moreKeySpec);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (additionalCount > 0 && additionalIndex == 0) {
|
|
|
|
// No '%' marker is found in more keys.
|
|
|
|
// Insert all additional more keys to the head of more keys.
|
|
|
|
if (DEBUG && out != null) {
|
|
|
|
throw new RuntimeException("Internal logic error:"
|
|
|
|
+ " moreKeys=" + Arrays.toString(moreKeys)
|
|
|
|
+ " additionalMoreKeys=" + Arrays.toString(additionalMoreKeys));
|
|
|
|
}
|
|
|
|
out = CollectionUtils.arrayAsList(additionalMoreKeys, additionalIndex, additionalCount);
|
|
|
|
for (int i = 0; i < moreKeysCount; i++) {
|
|
|
|
out.add(moreKeys[i]);
|
|
|
|
}
|
|
|
|
} else if (additionalIndex < additionalCount) {
|
|
|
|
// The number of '%' markers are less than additional more keys.
|
|
|
|
// Append remained additional more keys to the tail of more keys.
|
|
|
|
if (DEBUG && out != null) {
|
|
|
|
throw new RuntimeException("Internal logic error:"
|
|
|
|
+ " moreKeys=" + Arrays.toString(moreKeys)
|
|
|
|
+ " additionalMoreKeys=" + Arrays.toString(additionalMoreKeys));
|
|
|
|
}
|
|
|
|
out = CollectionUtils.arrayAsList(moreKeys, 0, moreKeysCount);
|
|
|
|
for (int i = additionalIndex; i < additionalCount; i++) {
|
|
|
|
out.add(additionalMoreKeys[additionalIndex]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (out == null && moreKeysCount > 0) {
|
|
|
|
return moreKeys;
|
|
|
|
} else if (out != null && out.size() > 0) {
|
|
|
|
return out.toArray(new String[out.size()]);
|
|
|
|
} else {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public static int getIntValue(final String[] moreKeys, final String key,
|
|
|
|
final int defaultValue) {
|
|
|
|
if (moreKeys == null) {
|
|
|
|
return defaultValue;
|
|
|
|
}
|
|
|
|
final int keyLen = key.length();
|
|
|
|
boolean foundValue = false;
|
|
|
|
int value = defaultValue;
|
|
|
|
for (int i = 0; i < moreKeys.length; i++) {
|
|
|
|
final String moreKeySpec = moreKeys[i];
|
|
|
|
if (moreKeySpec == null || !moreKeySpec.startsWith(key)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
moreKeys[i] = null;
|
|
|
|
try {
|
|
|
|
if (!foundValue) {
|
|
|
|
value = Integer.parseInt(moreKeySpec.substring(keyLen));
|
|
|
|
foundValue = true;
|
|
|
|
}
|
|
|
|
} catch (NumberFormatException e) {
|
|
|
|
throw new RuntimeException(
|
|
|
|
"integer should follow after " + key + ": " + moreKeySpec);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
|
|
|
|
public static boolean getBooleanValue(final String[] moreKeys, final String key) {
|
|
|
|
if (moreKeys == null) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
boolean value = false;
|
|
|
|
for (int i = 0; i < moreKeys.length; i++) {
|
|
|
|
final String moreKeySpec = moreKeys[i];
|
|
|
|
if (moreKeySpec == null || !moreKeySpec.equals(key)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
moreKeys[i] = null;
|
|
|
|
value = true;
|
|
|
|
}
|
|
|
|
return value;
|
|
|
|
}
|
2012-08-30 05:22:40 +00:00
|
|
|
}
|