diff options
Diffstat (limited to 'java/src/com/android')
27 files changed, 686 insertions, 493 deletions
diff --git a/java/src/com/android/inputmethod/keyboard/Key.java b/java/src/com/android/inputmethod/keyboard/Key.java index c8c4d30ef..f7eec6b43 100644 --- a/java/src/com/android/inputmethod/keyboard/Key.java +++ b/java/src/com/android/inputmethod/keyboard/Key.java @@ -284,19 +284,19 @@ public class Key implements Comparable<Key> { int moreKeysColumn = style.getInt(keyAttr, R.styleable.Keyboard_Key_maxMoreKeysColumn, params.mMaxMoreKeysKeyboardColumn); int value; - if ((value = KeySpecParser.getIntValue(moreKeys, MORE_KEYS_AUTO_COLUMN_ORDER, -1)) > 0) { + if ((value = MoreKeySpec.getIntValue(moreKeys, MORE_KEYS_AUTO_COLUMN_ORDER, -1)) > 0) { moreKeysColumn = value & MORE_KEYS_COLUMN_MASK; } - if ((value = KeySpecParser.getIntValue(moreKeys, MORE_KEYS_FIXED_COLUMN_ORDER, -1)) > 0) { + if ((value = MoreKeySpec.getIntValue(moreKeys, MORE_KEYS_FIXED_COLUMN_ORDER, -1)) > 0) { moreKeysColumn = MORE_KEYS_FLAGS_FIXED_COLUMN_ORDER | (value & MORE_KEYS_COLUMN_MASK); } - if (KeySpecParser.getBooleanValue(moreKeys, MORE_KEYS_HAS_LABELS)) { + if (MoreKeySpec.getBooleanValue(moreKeys, MORE_KEYS_HAS_LABELS)) { moreKeysColumn |= MORE_KEYS_FLAGS_HAS_LABELS; } - if (KeySpecParser.getBooleanValue(moreKeys, MORE_KEYS_NEEDS_DIVIDERS)) { + if (MoreKeySpec.getBooleanValue(moreKeys, MORE_KEYS_NEEDS_DIVIDERS)) { moreKeysColumn |= MORE_KEYS_FLAGS_NEEDS_DIVIDERS; } - if (KeySpecParser.getBooleanValue(moreKeys, MORE_KEYS_NO_PANEL_AUTO_MORE_KEY)) { + if (MoreKeySpec.getBooleanValue(moreKeys, MORE_KEYS_NO_PANEL_AUTO_MORE_KEY)) { moreKeysColumn |= MORE_KEYS_FLAGS_NO_PANEL_AUTO_MORE_KEY; } mMoreKeysColumnAndFlags = moreKeysColumn; @@ -308,7 +308,7 @@ public class Key implements Comparable<Key> { additionalMoreKeys = style.getStringArray(keyAttr, R.styleable.Keyboard_Key_additionalMoreKeys); } - moreKeys = KeySpecParser.insertAdditionalMoreKeys(moreKeys, additionalMoreKeys); + moreKeys = MoreKeySpec.insertAdditionalMoreKeys(moreKeys, additionalMoreKeys); if (moreKeys != null) { actionFlags |= ACTION_FLAGS_ENABLE_LONG_PRESS; mMoreKeys = new MoreKeySpec[moreKeys.length]; @@ -331,16 +331,16 @@ public class Key implements Comparable<Key> { // code point nor as a surrogate pair. mLabel = new StringBuilder().appendCodePoint(code).toString(); } else { - mLabel = KeySpecParser.toUpperCaseOfStringForLocale(style.getString(keyAttr, + mLabel = StringUtils.toUpperCaseOfStringForLocale(style.getString(keyAttr, R.styleable.Keyboard_Key_keyLabel), needsToUpperCase, locale); } if ((mLabelFlags & LABEL_FLAGS_DISABLE_HINT_LABEL) != 0) { mHintLabel = null; } else { - mHintLabel = KeySpecParser.toUpperCaseOfStringForLocale(style.getString(keyAttr, + mHintLabel = StringUtils.toUpperCaseOfStringForLocale(style.getString(keyAttr, R.styleable.Keyboard_Key_keyHintLabel), needsToUpperCase, locale); } - String outputText = KeySpecParser.toUpperCaseOfStringForLocale(style.getString(keyAttr, + String outputText = StringUtils.toUpperCaseOfStringForLocale(style.getString(keyAttr, R.styleable.Keyboard_Key_keyOutputText), needsToUpperCase, locale); // Choose the first letter of the label as primary code if not specified. if (code == CODE_UNSPECIFIED && TextUtils.isEmpty(outputText) @@ -367,9 +367,9 @@ public class Key implements Comparable<Key> { mCode = CODE_OUTPUT_TEXT; } } else { - mCode = KeySpecParser.toUpperCaseOfCodeForLocale(code, needsToUpperCase, locale); + mCode = StringUtils.toUpperCaseOfCodeForLocale(code, needsToUpperCase, locale); } - final int altCode = KeySpecParser.toUpperCaseOfCodeForLocale( + final int altCode = StringUtils.toUpperCaseOfCodeForLocale( KeySpecParser.parseCode(style.getString(keyAttr, R.styleable.Keyboard_Key_altCode), params.mCodesSet, CODE_UNSPECIFIED), needsToUpperCase, locale); diff --git a/java/src/com/android/inputmethod/keyboard/internal/CodesArrayParser.java b/java/src/com/android/inputmethod/keyboard/internal/CodesArrayParser.java index 4ccecb2f0..dce7fc57e 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/CodesArrayParser.java +++ b/java/src/com/android/inputmethod/keyboard/internal/CodesArrayParser.java @@ -17,6 +17,7 @@ package com.android.inputmethod.keyboard.internal; import com.android.inputmethod.latin.Constants; +import com.android.inputmethod.latin.utils.StringUtils; import android.text.TextUtils; @@ -29,15 +30,16 @@ import android.text.TextUtils; * marker. An output text may consist of multiple code points separated by comma. * The format of the codesArray element should be: * <pre> - * codePointInHex[,codePoint2InHex]*(|outputTextCodePointInHex[,outputTextCodePoint2InHex]*)? + * label1[,label2]*(|outputText1[,outputText2]*(|minSupportSdkVersion)?)? * </pre> */ // TODO: Write unit tests for this class. public final class CodesArrayParser { // Constants for parsing. - private static final char COMMA = ','; - private static final String VERTICAL_BAR_STRING = "\\|"; - private static final String COMMA_STRING = ","; + private static final char COMMA = Constants.CODE_COMMA; + private static final String COMMA_REGEX = StringUtils.newSingleCodePointString(COMMA); + private static final String VERTICAL_BAR_REGEX = // "\\|" + new String(new char[] { Constants.CODE_BACKSLASH, Constants.CODE_VERTICAL_BAR }); private static final int BASE_HEX = 16; private CodesArrayParser() { @@ -45,7 +47,7 @@ public final class CodesArrayParser { } private static String getLabelSpec(final String codesArraySpec) { - final String[] strs = codesArraySpec.split(VERTICAL_BAR_STRING, -1); + final String[] strs = codesArraySpec.split(VERTICAL_BAR_REGEX, -1); if (strs.length <= 1) { return codesArraySpec; } @@ -55,7 +57,7 @@ public final class CodesArrayParser { public static String parseLabel(final String codesArraySpec) { final String labelSpec = getLabelSpec(codesArraySpec); final StringBuilder sb = new StringBuilder(); - for (final String codeInHex : labelSpec.split(COMMA_STRING)) { + for (final String codeInHex : labelSpec.split(COMMA_REGEX)) { final int codePoint = Integer.parseInt(codeInHex, BASE_HEX); sb.appendCodePoint(codePoint); } @@ -63,17 +65,15 @@ public final class CodesArrayParser { } private static String getCodeSpec(final String codesArraySpec) { - final String[] strs = codesArraySpec.split(VERTICAL_BAR_STRING, -1); + final String[] strs = codesArraySpec.split(VERTICAL_BAR_REGEX, -1); if (strs.length <= 1) { return codesArraySpec; } return TextUtils.isEmpty(strs[1]) ? strs[0] : strs[1]; } - // codesArraySpec consists of: - // <label>|<code0>,<code1>,...|<minSupportSdkVersion> public static int getMinSupportSdkVersion(final String codesArraySpec) { - final String[] strs = codesArraySpec.split(VERTICAL_BAR_STRING, -1); + final String[] strs = codesArraySpec.split(VERTICAL_BAR_REGEX, -1); if (strs.length <= 2) { return 0; } @@ -98,7 +98,7 @@ public final class CodesArrayParser { return null; } final StringBuilder sb = new StringBuilder(); - for (final String codeInHex : codeSpec.split(COMMA_STRING)) { + for (final String codeInHex : codeSpec.split(COMMA_REGEX)) { final int codePoint = Integer.parseInt(codeInHex, BASE_HEX); sb.appendCodePoint(codePoint); } diff --git a/java/src/com/android/inputmethod/keyboard/internal/EmojiPageKeyboardView.java b/java/src/com/android/inputmethod/keyboard/internal/EmojiPageKeyboardView.java index 5c7c6e39d..be7396520 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/EmojiPageKeyboardView.java +++ b/java/src/com/android/inputmethod/keyboard/internal/EmojiPageKeyboardView.java @@ -17,6 +17,7 @@ package com.android.inputmethod.keyboard.internal; import android.content.Context; +import android.os.Handler; import android.util.AttributeSet; import android.view.GestureDetector; import android.view.MotionEvent; @@ -35,16 +36,19 @@ import com.android.inputmethod.latin.R; // TODO: Implement key popup preview. public final class EmojiPageKeyboardView extends KeyboardView implements GestureDetector.OnGestureListener { + private static final long KEY_PRESS_DELAY_TIME = 250; // msec + private static final long KEY_RELEASE_DELAY_TIME = 30; // msec + public interface OnKeyEventListener { public void onPressKey(Key key); public void onReleaseKey(Key key); } private static final OnKeyEventListener EMPTY_LISTENER = new OnKeyEventListener() { - @Override - public void onPressKey(final Key key) {} - @Override - public void onReleaseKey(final Key key) {} + @Override + public void onPressKey(final Key key) {} + @Override + public void onReleaseKey(final Key key) {} }; private OnKeyEventListener mListener = EMPTY_LISTENER; @@ -60,6 +64,7 @@ public final class EmojiPageKeyboardView extends KeyboardView implements super(context, attrs, defStyle); mGestureDetector = new GestureDetector(context, this); mGestureDetector.setIsLongpressEnabled(false /* isLongpressEnabled */); + mHandler = new Handler(); } public void setOnKeyEventListener(final OnKeyEventListener listener) { @@ -92,6 +97,8 @@ public final class EmojiPageKeyboardView extends KeyboardView implements // {@link GestureEnabler#OnGestureListener} methods. private Key mCurrentKey; + private Runnable mPendingKeyDown; + private final Handler mHandler; private Key getKey(final MotionEvent e) { final int index = e.getActionIndex(); @@ -101,6 +108,8 @@ public final class EmojiPageKeyboardView extends KeyboardView implements } public void releaseCurrentKey() { + mHandler.removeCallbacks(mPendingKeyDown); + mPendingKeyDown = null; final Key currentKey = mCurrentKey; if (currentKey == null) { return; @@ -118,9 +127,17 @@ public final class EmojiPageKeyboardView extends KeyboardView implements if (key == null) { return false; } - key.onPressed(); - invalidateKey(key); - mListener.onPressKey(key); + // Do not trigger key-down effect right now in case this is actually a fling action. + mPendingKeyDown = new Runnable() { + @Override + public void run() { + mPendingKeyDown = null; + key.onPressed(); + invalidateKey(key); + mListener.onPressKey(key); + } + }; + mHandler.postDelayed(mPendingKeyDown, KEY_PRESS_DELAY_TIME); return false; } @@ -132,13 +149,28 @@ public final class EmojiPageKeyboardView extends KeyboardView implements @Override public boolean onSingleTapUp(final MotionEvent e) { final Key key = getKey(e); + final Runnable pendingKeyDown = mPendingKeyDown; + final Key currentKey = mCurrentKey; releaseCurrentKey(); if (key == null) { return false; } - key.onReleased(); - invalidateKey(key); - mListener.onReleaseKey(key); + if (key == currentKey && pendingKeyDown != null) { + pendingKeyDown.run(); + // Trigger key-release event a little later so that a user can see visual feedback. + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + key.onReleased(); + invalidateKey(key); + mListener.onReleaseKey(key); + } + }, KEY_RELEASE_DELAY_TIME); + } else { + key.onReleased(); + invalidateKey(key); + mListener.onReleaseKey(key); + } return true; } diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java b/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java index accfaedcb..c51941d32 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java @@ -19,114 +19,47 @@ package com.android.inputmethod.keyboard.internal; import static com.android.inputmethod.latin.Constants.CODE_OUTPUT_TEXT; import static com.android.inputmethod.latin.Constants.CODE_UNSPECIFIED; -import android.text.TextUtils; - import com.android.inputmethod.latin.Constants; -import com.android.inputmethod.latin.LatinImeLogger; -import com.android.inputmethod.latin.utils.CollectionUtils; import com.android.inputmethod.latin.utils.StringUtils; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Locale; - /** - * The string parser of more keys specification. - * The specification is comma separated texts each of which represents one "more key". - * The specification might have label or string resource reference in it. These references are - * expanded before parsing comma. - * - Label reference should be a string representation of label (!text/label_name) - * - String resource reference should be a string representation of resource (!text/resource_name) - * Each "more key" specification is one of the following: - * - Label optionally followed by keyOutputText or code (keyLabel|keyOutputText). - * - Icon followed by keyOutputText or code (!icon/icon_name|!code/code_name) - * - Icon should be a string representation of icon (!icon/icon_name). - * - Code should be a code point presented by hexadecimal string prefixed with "0x", or a string - * representation of code (!code/code_name). + * The string parser of the key specification. + * + * Each key specification is one of the following: + * - Label optionally followed by keyOutputText (keyLabel|keyOutputText). + * - Label optionally followed by code point (keyLabel|!code/code_name). + * - Icon followed by keyOutputText (!icon/icon_name|keyOutputText). + * - Icon followed by code point (!icon/icon_name|!code/code_name). + * Label and keyOutputText are one of the following: + * - Literal string. + * - Label reference represented by (!text/label_name), see {@link KeyboardTextsSet}. + * - String resource reference represented by (!text/resource_name), see {@link KeyboardTextsSet}. + * Icon is represented by (!icon/icon_name), see {@link KeyboardIconsSet}. + * Code is one of the following: + * - Code point presented by hexadecimal string prefixed with "0x" + * - Code reference represented by (!code/code_name), see {@link KeyboardCodesSet}. * Special character, comma ',' backslash '\', and bar '|' can be escaped by '\' character. - * Note that the '\' is also parsed by XML parser and CSV parser as well. - * See {@link KeyboardIconsSet} about icon_name. + * Note that the '\' is also parsed by XML parser and {@link MoreKeySpec#splitKeySpecs(String)} + * as well. */ public final class KeySpecParser { - private static final boolean DEBUG = LatinImeLogger.sDBG; - - private static final int MAX_STRING_REFERENCE_INDIRECTION = 10; - // Constants for parsing. - private static final char COMMA = ','; - private static final char BACKSLASH = '\\'; - private static final char VERTICAL_BAR = '|'; - private static final String PREFIX_TEXT = "!text/"; - static final String PREFIX_ICON = "!icon/"; - private static final String PREFIX_CODE = "!code/"; + private static final char BACKSLASH = Constants.CODE_BACKSLASH; + private static final char VERTICAL_BAR = Constants.CODE_VERTICAL_BAR; private static final String PREFIX_HEX = "0x"; - private static final String ADDITIONAL_MORE_KEY_MARKER = "%"; private KeySpecParser() { // Intentional empty constructor for utility class. } - /** - * 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) { - list = CollectionUtils.newArrayList(); - } - 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 boolean hasIcon(final String keySpec) { + return keySpec.startsWith(KeyboardIconsSet.PREFIX_ICON); } - private static boolean hasIcon(final String moreKeySpec) { - return moreKeySpec.startsWith(PREFIX_ICON); - } - - private static boolean hasCode(final String moreKeySpec) { - final int end = indexOfLabelEnd(moreKeySpec, 0); - if (end > 0 && end + 1 < moreKeySpec.length() && moreKeySpec.startsWith( - PREFIX_CODE, end + 1)) { + private static boolean hasCode(final String keySpec) { + final int end = indexOfLabelEnd(keySpec, 0); + if (end > 0 && end + 1 < keySpec.length() && keySpec.startsWith( + KeyboardCodesSet.PREFIX_CODE, end + 1)) { return true; } return false; @@ -151,17 +84,17 @@ public final class KeySpecParser { return sb.toString(); } - private static int indexOfLabelEnd(final String moreKeySpec, final int start) { - if (moreKeySpec.indexOf(BACKSLASH, start) < 0) { - final int end = moreKeySpec.indexOf(VERTICAL_BAR, start); + private static int indexOfLabelEnd(final String keySpec, final int start) { + if (keySpec.indexOf(BACKSLASH, start) < 0) { + final int end = keySpec.indexOf(VERTICAL_BAR, start); if (end == 0) { - throw new KeySpecParserError(VERTICAL_BAR + " at " + start + ": " + moreKeySpec); + throw new KeySpecParserError(VERTICAL_BAR + " at " + start + ": " + keySpec); } return end; } - final int length = moreKeySpec.length(); + final int length = keySpec.length(); for (int pos = start; pos < length; pos++) { - final char c = moreKeySpec.charAt(pos); + final char c = keySpec.charAt(pos); if (c == BACKSLASH && pos + 1 < length) { // Skip escape char pos++; @@ -172,63 +105,63 @@ public final class KeySpecParser { return -1; } - public static String getLabel(final String moreKeySpec) { - if (hasIcon(moreKeySpec)) { + public static String getLabel(final String keySpec) { + if (hasIcon(keySpec)) { return null; } - final int end = indexOfLabelEnd(moreKeySpec, 0); - final String label = (end > 0) ? parseEscape(moreKeySpec.substring(0, end)) - : parseEscape(moreKeySpec); - if (TextUtils.isEmpty(label)) { - throw new KeySpecParserError("Empty label: " + moreKeySpec); + final int end = indexOfLabelEnd(keySpec, 0); + final String label = (end > 0) ? parseEscape(keySpec.substring(0, end)) + : parseEscape(keySpec); + if (label.isEmpty()) { + throw new KeySpecParserError("Empty label: " + keySpec); } return label; } - private static String getOutputTextInternal(final String moreKeySpec) { - final int end = indexOfLabelEnd(moreKeySpec, 0); + private static String getOutputTextInternal(final String keySpec) { + final int end = indexOfLabelEnd(keySpec, 0); if (end <= 0) { return null; } - if (indexOfLabelEnd(moreKeySpec, end + 1) >= 0) { - throw new KeySpecParserError("Multiple " + VERTICAL_BAR + ": " + moreKeySpec); + if (indexOfLabelEnd(keySpec, end + 1) >= 0) { + throw new KeySpecParserError("Multiple " + VERTICAL_BAR + ": " + keySpec); } - return parseEscape(moreKeySpec.substring(end + /* VERTICAL_BAR */1)); + return parseEscape(keySpec.substring(end + /* VERTICAL_BAR */1)); } - static String getOutputText(final String moreKeySpec) { - if (hasCode(moreKeySpec)) { + public static String getOutputText(final String keySpec) { + if (hasCode(keySpec)) { return null; } - final String outputText = getOutputTextInternal(moreKeySpec); + final String outputText = getOutputTextInternal(keySpec); if (outputText != null) { if (StringUtils.codePointCount(outputText) == 1) { // If output text is one code point, it should be treated as a code. // See {@link #getCode(Resources, String)}. return null; } - if (!TextUtils.isEmpty(outputText)) { - return outputText; + if (outputText.isEmpty()) { + throw new KeySpecParserError("Empty outputText: " + keySpec); } - throw new KeySpecParserError("Empty outputText: " + moreKeySpec); + return outputText; } - final String label = getLabel(moreKeySpec); + final String label = getLabel(keySpec); if (label == null) { - throw new KeySpecParserError("Empty label: " + moreKeySpec); + throw new KeySpecParserError("Empty label: " + keySpec); } // Code is automatically generated for one letter label. See {@link getCode()}. return (StringUtils.codePointCount(label) == 1) ? null : label; } - static int getCode(final String moreKeySpec, final KeyboardCodesSet codesSet) { - if (hasCode(moreKeySpec)) { - final int end = indexOfLabelEnd(moreKeySpec, 0); - if (indexOfLabelEnd(moreKeySpec, end + 1) >= 0) { - throw new KeySpecParserError("Multiple " + VERTICAL_BAR + ": " + moreKeySpec); + public static int getCode(final String keySpec, final KeyboardCodesSet codesSet) { + if (hasCode(keySpec)) { + final int end = indexOfLabelEnd(keySpec, 0); + if (indexOfLabelEnd(keySpec, end + 1) >= 0) { + throw new KeySpecParserError("Multiple " + VERTICAL_BAR + ": " + keySpec); } - return parseCode(moreKeySpec.substring(end + 1), codesSet, CODE_UNSPECIFIED); + return parseCode(keySpec.substring(end + 1), codesSet, CODE_UNSPECIFIED); } - final String outputText = getOutputTextInternal(moreKeySpec); + final String outputText = getOutputTextInternal(keySpec); if (outputText != null) { // If output text is one code point, it should be treated as a code. // See {@link #getOutputText(String)}. @@ -237,7 +170,7 @@ public final class KeySpecParser { } return CODE_OUTPUT_TEXT; } - final String label = getLabel(moreKeySpec); + final String label = getLabel(keySpec); // Code is automatically generated for one letter label. if (StringUtils.codePointCount(label) == 1) { return label.codePointAt(0); @@ -248,8 +181,8 @@ public final class KeySpecParser { public static int parseCode(final String text, final KeyboardCodesSet codesSet, final int defCode) { if (text == null) return defCode; - if (text.startsWith(PREFIX_CODE)) { - return codesSet.getCode(text.substring(PREFIX_CODE.length())); + if (text.startsWith(KeyboardCodesSet.PREFIX_CODE)) { + return codesSet.getCode(text.substring(KeyboardCodesSet.PREFIX_CODE.length())); } else if (text.startsWith(PREFIX_HEX)) { return Integer.parseInt(text.substring(PREFIX_HEX.length()), 16); } else { @@ -257,245 +190,22 @@ public final class KeySpecParser { } } - public static int getIconId(final String moreKeySpec) { - if (moreKeySpec != null && hasIcon(moreKeySpec)) { - final int end = moreKeySpec.indexOf(VERTICAL_BAR, PREFIX_ICON.length()); - final String name = (end < 0) ? moreKeySpec.substring(PREFIX_ICON.length()) - : moreKeySpec.substring(PREFIX_ICON.length(), end); + public static int getIconId(final String keySpec) { + if (keySpec != null && hasIcon(keySpec)) { + final int end = keySpec.indexOf( + VERTICAL_BAR, KeyboardIconsSet.PREFIX_ICON.length()); + final String name = (end < 0) + ? keySpec.substring(KeyboardIconsSet.PREFIX_ICON.length()) + : keySpec.substring(KeyboardIconsSet.PREFIX_ICON.length(), end); return KeyboardIconsSet.getIconId(name); } return KeyboardIconsSet.ICON_UNDEFINED; } - private static <T> ArrayList<T> arrayAsList(final T[] array, final int start, final int end) { - if (array == null) { - throw new NullPointerException(); - } - if (start < 0 || start > end || end > array.length) { - throw new IllegalArgumentException(); - } - - final ArrayList<T> list = CollectionUtils.newArrayList(end - start); - for (int i = start; i < end; i++) { - list.add(array[i]); - } - return list; - } - - 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 = 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 = 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 = 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 = 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; - } - } - @SuppressWarnings("serial") public static final class KeySpecParserError extends RuntimeException { public KeySpecParserError(final String message) { super(message); } } - - public static String resolveTextReference(final String rawText, - final KeyboardTextsSet textsSet) { - if (TextUtils.isEmpty(rawText)) { - return null; - } - int level = 0; - String text = rawText; - StringBuilder sb; - do { - level++; - if (level >= MAX_STRING_REFERENCE_INDIRECTION) { - throw new RuntimeException("too many @string/resource indirection: " + text); - } - - final int prefixLen = PREFIX_TEXT.length(); - final int size = text.length(); - if (size < prefixLen) { - return TextUtils.isEmpty(text) ? null : text; - } - - sb = null; - for (int pos = 0; pos < size; pos++) { - final char c = text.charAt(pos); - if (text.startsWith(PREFIX_TEXT, pos) && textsSet != null) { - if (sb == null) { - sb = new StringBuilder(text.substring(0, pos)); - } - final int end = searchTextNameEnd(text, pos + prefixLen); - final String name = text.substring(pos + prefixLen, end); - sb.append(textsSet.getText(name)); - pos = end - 1; - } else if (c == BACKSLASH) { - if (sb != null) { - // Append both escape character and escaped character. - sb.append(text.substring(pos, Math.min(pos + 2, size))); - } - pos++; - } else if (sb != null) { - sb.append(c); - } - } - - if (sb != null) { - text = sb.toString(); - } - } while (sb != null); - return TextUtils.isEmpty(text) ? null : text; - } - - private static int searchTextNameEnd(final String text, final int start) { - final int size = text.length(); - for (int pos = start; pos < size; pos++) { - final char c = text.charAt(pos); - // Label name should be consisted of [a-zA-Z_0-9]. - if ((c >= 'a' && c <= 'z') || c == '_' || (c >= '0' && c <= '9')) { - continue; - } - return pos; - } - return size; - } - - 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; - } - - public static int toUpperCaseOfCodeForLocale(final int code, final boolean needsToUpperCase, - final Locale locale) { - if (!Constants.isLetterCode(code) || !needsToUpperCase) return code; - final String text = StringUtils.newSingleCodePointString(code); - final String casedText = KeySpecParser.toUpperCaseOfStringForLocale( - text, needsToUpperCase, locale); - return StringUtils.codePointCount(casedText) == 1 - ? casedText.codePointAt(0) : CODE_UNSPECIFIED; - } - - public static String toUpperCaseOfStringForLocale(final String text, - final boolean needsToUpperCase, final Locale locale) { - if (text == null || !needsToUpperCase) return text; - return text.toUpperCase(locale); - } } diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java b/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java index e6a674334..7941ddd41 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java @@ -32,15 +32,15 @@ public abstract class KeyStyle { protected String parseString(final TypedArray a, final int index) { if (a.hasValue(index)) { - return KeySpecParser.resolveTextReference(a.getString(index), mTextsSet); + return mTextsSet.resolveTextReference(a.getString(index)); } return null; } protected String[] parseStringArray(final TypedArray a, final int index) { if (a.hasValue(index)) { - final String text = KeySpecParser.resolveTextReference(a.getString(index), mTextsSet); - return KeySpecParser.splitKeySpecs(text); + final String text = mTextsSet.resolveTextReference(a.getString(index)); + return MoreKeySpec.splitKeySpecs(text); } return null; } diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java index 78809d53b..aeb4db557 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java @@ -22,6 +22,8 @@ import com.android.inputmethod.latin.utils.CollectionUtils; import java.util.HashMap; public final class KeyboardCodesSet { + public static final String PREFIX_CODE = "!code/"; + private static final HashMap<String, int[]> sLanguageToCodesMap = CollectionUtils.newHashMap(); private static final HashMap<String, Integer> sNameToIdMap = CollectionUtils.newHashMap(); diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java index 0ee935f60..da8bf7d69 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java @@ -30,6 +30,7 @@ import java.util.HashMap; public final class KeyboardIconsSet { private static final String TAG = KeyboardIconsSet.class.getSimpleName(); + public static final String PREFIX_ICON = "!icon/"; public static final int ICON_UNDEFINED = 0; private static final int ATTR_UNDEFINED = 0; diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java index 4e8eb3ea7..f8c098893 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java @@ -18,8 +18,10 @@ package com.android.inputmethod.keyboard.internal; import android.content.Context; import android.content.res.Resources; +import android.text.TextUtils; import com.android.inputmethod.annotations.UsedForTesting; +import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.utils.CollectionUtils; import java.util.HashMap; @@ -45,6 +47,10 @@ import java.util.HashMap; * KeyboardTextsSet.java */ public final class KeyboardTextsSet { + public static final String PREFIX_TEXT = "!text/"; + private static final char BACKSLASH = Constants.CODE_BACKSLASH; + private static final int MAX_STRING_REFERENCE_INDIRECTION = 10; + // Language to texts map. private static final HashMap<String, String[]> sLocaleToTextsMap = CollectionUtils.newHashMap(); private static final HashMap<String, Integer> sNameToIdsMap = CollectionUtils.newHashMap(); @@ -87,6 +93,67 @@ public final class KeyboardTextsSet { return (text == null) ? LANGUAGE_DEFAULT[id] : text; } + private static int searchTextNameEnd(final String text, final int start) { + final int size = text.length(); + for (int pos = start; pos < size; pos++) { + final char c = text.charAt(pos); + // Label name should be consisted of [a-zA-Z_0-9]. + if ((c >= 'a' && c <= 'z') || c == '_' || (c >= '0' && c <= '9')) { + continue; + } + return pos; + } + return size; + } + + public String resolveTextReference(final String rawText) { + if (TextUtils.isEmpty(rawText)) { + return null; + } + int level = 0; + String text = rawText; + StringBuilder sb; + do { + level++; + if (level >= MAX_STRING_REFERENCE_INDIRECTION) { + throw new RuntimeException("too many @string/resource indirection: " + text); + } + + final int prefixLen = PREFIX_TEXT.length(); + final int size = text.length(); + if (size < prefixLen) { + return TextUtils.isEmpty(text) ? null : text; + } + + sb = null; + for (int pos = 0; pos < size; pos++) { + final char c = text.charAt(pos); + if (text.startsWith(PREFIX_TEXT, pos)) { + if (sb == null) { + sb = new StringBuilder(text.substring(0, pos)); + } + final int end = searchTextNameEnd(text, pos + prefixLen); + final String name = text.substring(pos + prefixLen, end); + sb.append(getText(name)); + pos = end - 1; + } else if (c == BACKSLASH) { + if (sb != null) { + // Append both escape character and escaped character. + sb.append(text.substring(pos, Math.min(pos + 2, size))); + } + pos++; + } else if (sb != null) { + sb.append(c); + } + } + + if (sb != null) { + text = sb.toString(); + } + } while (sb != null); + return TextUtils.isEmpty(text) ? null : text; + } + // These texts' name should be aligned with the @string/<name> in // values*/strings-action-keys.xml. private static final String[] RESOURCE_NAMES = { diff --git a/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java b/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java index 110936f8f..d3bc0c2b2 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java +++ b/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java @@ -19,10 +19,25 @@ package com.android.inputmethod.keyboard.internal; import android.text.TextUtils; import com.android.inputmethod.latin.Constants; +import com.android.inputmethod.latin.LatinImeLogger; +import com.android.inputmethod.latin.utils.CollectionUtils; import com.android.inputmethod.latin.utils.StringUtils; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Locale; +/** + * 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. + */ public final class MoreKeySpec { public final int mCode; public final String mLabel; @@ -31,9 +46,9 @@ public final class MoreKeySpec { public MoreKeySpec(final String moreKeySpec, boolean needsToUpperCase, final Locale locale, final KeyboardCodesSet codesSet) { - mLabel = KeySpecParser.toUpperCaseOfStringForLocale( + mLabel = StringUtils.toUpperCaseOfStringForLocale( KeySpecParser.getLabel(moreKeySpec), needsToUpperCase, locale); - final int code = KeySpecParser.toUpperCaseOfCodeForLocale( + final int code = StringUtils.toUpperCaseOfCodeForLocale( KeySpecParser.getCode(moreKeySpec, codesSet), needsToUpperCase, locale); if (code == Constants.CODE_UNSPECIFIED) { // Some letter, for example German Eszett (U+00DF: "ß"), has multiple characters @@ -42,7 +57,7 @@ public final class MoreKeySpec { mOutputText = mLabel; } else { mCode = code; - mOutputText = KeySpecParser.toUpperCaseOfStringForLocale( + mOutputText = StringUtils.toUpperCaseOfStringForLocale( KeySpecParser.getOutputText(moreKeySpec), needsToUpperCase, locale); } mIconId = KeySpecParser.getIconId(moreKeySpec); @@ -74,7 +89,7 @@ public final class MoreKeySpec { @Override public String toString() { final String label = (mIconId == KeyboardIconsSet.ICON_UNDEFINED ? mLabel - : KeySpecParser.PREFIX_ICON + KeyboardIconsSet.getIconName(mIconId)); + : KeyboardIconsSet.PREFIX_ICON + KeyboardIconsSet.getIconName(mIconId)); final String output = (mCode == Constants.CODE_OUTPUT_TEXT ? mOutputText : Constants.printableCode(mCode)); if (StringUtils.codePointCount(label) == 1 && label.codePointAt(0) == mCode) { @@ -83,4 +98,196 @@ public final class MoreKeySpec { return label + "|" + output; } } + + private static final boolean DEBUG = LatinImeLogger.sDBG; + // 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) { + list = CollectionUtils.newArrayList(); + } + 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; + } } diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java index e66cfca49..00eb57c9f 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java @@ -22,12 +22,13 @@ import android.util.SparseArray; import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.keyboard.ProximityInfo; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; +import com.android.inputmethod.latin.makedict.Word; import com.android.inputmethod.latin.settings.NativeSuggestOptions; import com.android.inputmethod.latin.utils.CollectionUtils; import com.android.inputmethod.latin.utils.JniUtils; import com.android.inputmethod.latin.utils.LanguageModelParam; import com.android.inputmethod.latin.utils.StringUtils; -import com.android.inputmethod.latin.utils.UnigramProperty; +import com.android.inputmethod.latin.utils.WordProperty; import java.io.File; import java.util.ArrayList; @@ -61,18 +62,19 @@ public final class BinaryDictionary extends Dictionary { public static final int NOT_A_VALID_TIMESTAMP = -1; - // Format to get unigram flags from native side via getUnigramPropertyNative(). - private static final int FORMAT_UNIGRAM_PROPERTY_OUTPUT_FLAG_COUNT = 4; - private static final int FORMAT_UNIGRAM_PROPERTY_IS_NOT_A_WORD_INDEX = 0; - private static final int FORMAT_UNIGRAM_PROPERTY_IS_BLACKLISTED_INDEX = 1; - private static final int FORMAT_UNIGRAM_PROPERTY_HAS_BIGRAMS_INDEX = 2; - private static final int FORMAT_UNIGRAM_PROPERTY_HAS_SHORTCUTS_INDEX = 3; + // Format to get unigram flags from native side via getWordPropertyNative(). + private static final int FORMAT_WORD_PROPERTY_OUTPUT_FLAG_COUNT = 4; + private static final int FORMAT_WORD_PROPERTY_IS_NOT_A_WORD_INDEX = 0; + private static final int FORMAT_WORD_PROPERTY_IS_BLACKLISTED_INDEX = 1; + private static final int FORMAT_WORD_PROPERTY_HAS_BIGRAMS_INDEX = 2; + private static final int FORMAT_WORD_PROPERTY_HAS_SHORTCUTS_INDEX = 3; - // Format to get unigram historical info from native side via getUnigramPropertyNative(). - private static final int FORMAT_UNIGRAM_PROPERTY_OUTPUT_HISTORICAL_INFO_COUNT = 3; - private static final int FORMAT_UNIGRAM_PROPERTY_TIMESTAMP_INDEX = 0; - private static final int FORMAT_UNIGRAM_PROPERTY_LEVEL_INDEX = 1; - private static final int FORMAT_UNIGRAM_PROPERTY_COUNT_INDEX = 2; + // Format to get probability and historical info from native side via getWordPropertyNative(). + public static final int FORMAT_WORD_PROPERTY_OUTPUT_PROBABILITY_INFO_COUNT = 4; + public static final int FORMAT_WORD_PROPERTY_PROBABILITY_INDEX = 0; + public static final int FORMAT_WORD_PROPERTY_TIMESTAMP_INDEX = 1; + public static final int FORMAT_WORD_PROPERTY_LEVEL_INDEX = 2; + public static final int FORMAT_WORD_PROPERTY_COUNT_INDEX = 3; private long mNativeDict; private final Locale mLocale; @@ -143,10 +145,11 @@ public final class BinaryDictionary extends Dictionary { private static native int getFormatVersionNative(long dict); private static native int getProbabilityNative(long dict, int[] word); private static native int getBigramProbabilityNative(long dict, int[] word0, int[] word1); - private static native void getUnigramPropertyNative(long dict, int[] word, - int[] outCodePoints, boolean[] outFlags, int[] outProbability, - int[] outHistoricalInfo, ArrayList<int[]> outShortcutTargets, - ArrayList<Integer> outShortcutProbabilities); + private static native void getWordPropertyNative(long dict, int[] word, + int[] outCodePoints, boolean[] outFlags, int[] outProbabilityInfo, + ArrayList<int[]> outBigramTargets, ArrayList<int[]> outBigramProbabilityInfo, + ArrayList<int[]> outShortcutTargets, ArrayList<Integer> outShortcutProbabilities); + private static native int getNextWordNative(long dict, int token, int[] outCodePoints); private static native int getSuggestionsNative(long dict, long proximityInfo, long traverseSession, int[] xCoordinates, int[] yCoordinates, int[] times, int[] pointerIds, int[] inputCodePoints, int inputSize, int commitPoint, @@ -306,29 +309,56 @@ public final class BinaryDictionary extends Dictionary { } @UsedForTesting - public UnigramProperty getUnigramProperty(final String word) { + public WordProperty getWordProperty(final String word) { if (TextUtils.isEmpty(word)) { return null; } final int[] codePoints = StringUtils.toCodePointArray(word); final int[] outCodePoints = new int[MAX_WORD_LENGTH]; - final boolean[] outFlags = new boolean[FORMAT_UNIGRAM_PROPERTY_OUTPUT_FLAG_COUNT]; - final int[] outProbability = new int[1]; - final int[] outHistoricalInfo = - new int[FORMAT_UNIGRAM_PROPERTY_OUTPUT_HISTORICAL_INFO_COUNT]; + final boolean[] outFlags = new boolean[FORMAT_WORD_PROPERTY_OUTPUT_FLAG_COUNT]; + final int[] outProbabilityInfo = + new int[FORMAT_WORD_PROPERTY_OUTPUT_PROBABILITY_INFO_COUNT]; + final ArrayList<int[]> outBigramTargets = CollectionUtils.newArrayList(); + final ArrayList<int[]> outBigramProbabilityInfo = CollectionUtils.newArrayList(); final ArrayList<int[]> outShortcutTargets = CollectionUtils.newArrayList(); final ArrayList<Integer> outShortcutProbabilities = CollectionUtils.newArrayList(); - getUnigramPropertyNative(mNativeDict, codePoints, outCodePoints, outFlags, outProbability, - outHistoricalInfo, outShortcutTargets, outShortcutProbabilities); - return new UnigramProperty(codePoints, - outFlags[FORMAT_UNIGRAM_PROPERTY_IS_NOT_A_WORD_INDEX], - outFlags[FORMAT_UNIGRAM_PROPERTY_IS_BLACKLISTED_INDEX], - outFlags[FORMAT_UNIGRAM_PROPERTY_HAS_BIGRAMS_INDEX], - outFlags[FORMAT_UNIGRAM_PROPERTY_HAS_SHORTCUTS_INDEX], outProbability[0], - outHistoricalInfo[FORMAT_UNIGRAM_PROPERTY_TIMESTAMP_INDEX], - outHistoricalInfo[FORMAT_UNIGRAM_PROPERTY_LEVEL_INDEX], - outHistoricalInfo[FORMAT_UNIGRAM_PROPERTY_COUNT_INDEX], - outShortcutTargets, outShortcutProbabilities); + getWordPropertyNative(mNativeDict, codePoints, outCodePoints, outFlags, outProbabilityInfo, + outBigramTargets, outBigramProbabilityInfo, outShortcutTargets, + outShortcutProbabilities); + return new WordProperty(codePoints, + outFlags[FORMAT_WORD_PROPERTY_IS_NOT_A_WORD_INDEX], + outFlags[FORMAT_WORD_PROPERTY_IS_BLACKLISTED_INDEX], + outFlags[FORMAT_WORD_PROPERTY_HAS_BIGRAMS_INDEX], + outFlags[FORMAT_WORD_PROPERTY_HAS_SHORTCUTS_INDEX], outProbabilityInfo, + outBigramTargets, outBigramProbabilityInfo, outShortcutTargets, + outShortcutProbabilities); + } + + public static class GetNextWordPropertyResult { + public WordProperty mWordProperty; + public int mNextToken; + + public GetNextWordPropertyResult(final WordProperty wordPreperty, final int nextToken) { + mWordProperty = wordPreperty; + mNextToken = nextToken; + } + } + + /** + * Method to iterate all words in the dictionary for makedict. + * If token is 0, this method newly starts iterating the dictionary. + */ + @UsedForTesting + public GetNextWordPropertyResult getNextWordProperty(final int token) { + final int[] codePoints = new int[MAX_WORD_LENGTH]; + final int nextToken = getNextWordNative(mNativeDict, token, codePoints); + int len = 0; + // codePoints is null-terminated if its length is shorter than the array length. + while (len < MAX_WORD_LENGTH && codePoints[len] != 0) { + ++len; + } + final String word = new String(mOutputCodePoints, 0, len); + return new GetNextWordPropertyResult(getWordProperty(word), nextToken); } // Add a unigram entry to binary dictionary with unigram attributes in native code. @@ -379,7 +409,6 @@ public final class BinaryDictionary extends Dictionary { return; } } - } private void reopen() { diff --git a/java/src/com/android/inputmethod/latin/Constants.java b/java/src/com/android/inputmethod/latin/Constants.java index 0b396b1de..d6ac71fe2 100644 --- a/java/src/com/android/inputmethod/latin/Constants.java +++ b/java/src/com/android/inputmethod/latin/Constants.java @@ -191,6 +191,8 @@ public final class Constants { public static final int CODE_QUESTION_MARK = '?'; public static final int CODE_EXCLAMATION_MARK = '!'; public static final int CODE_SLASH = '/'; + public static final int CODE_BACKSLASH = '\\'; + public static final int CODE_VERTICAL_BAR = '|'; public static final int CODE_COMMERCIAL_AT = '@'; public static final int CODE_PLUS = '+'; public static final int CODE_PERCENT = '%'; diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java index 8e1675b2a..4dee84a7b 100644 --- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java @@ -267,9 +267,9 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { protected Map<String, String> getHeaderAttributeMap() { HashMap<String, String> attributeMap = new HashMap<String, String>(); - attributeMap.put(FormatSpec.FileHeader.DICTIONARY_ID_ATTRIBUTE, mDictName); - attributeMap.put(FormatSpec.FileHeader.DICTIONARY_LOCALE_ATTRIBUTE, mLocale.toString()); - attributeMap.put(FormatSpec.FileHeader.DICTIONARY_VERSION_ATTRIBUTE, + attributeMap.put(FormatSpec.FileHeader.DICTIONARY_ID_KEY, mDictName); + attributeMap.put(FormatSpec.FileHeader.DICTIONARY_LOCALE_KEY, mLocale.toString()); + attributeMap.put(FormatSpec.FileHeader.DICTIONARY_VERSION_KEY, String.valueOf(TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()))); return attributeMap; } diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index 8f4b2d67e..6517ef29d 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -79,6 +79,7 @@ import com.android.inputmethod.latin.utils.ApplicationUtils; import com.android.inputmethod.latin.utils.CapsModeUtils; import com.android.inputmethod.latin.utils.CompletionInfoUtils; import com.android.inputmethod.latin.utils.CoordinateUtils; +import com.android.inputmethod.latin.utils.ImportantNoticeUtils; import com.android.inputmethod.latin.utils.IntentUtils; import com.android.inputmethod.latin.utils.JniUtils; import com.android.inputmethod.latin.utils.LatinImeLoggerUtils; @@ -1157,6 +1158,27 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mInputLogic.mSuggest.mDictionaryFacilitator.addWordToUserDictionary(wordToEdit); } + // TODO: Move this method out of {@link LatinIME}. + // Callback for the {@link SuggestionStripView}, to call when the important notice strip is + // pressed. + @Override + public void showImportantNoticeContents() { + final Context context = this; + final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { + @Override + public void onClick(final DialogInterface di, final int position) { + di.dismiss(); + ImportantNoticeUtils.updateLastImportantNoticeVersion(context); + setNeutralSuggestionStrip(); + } + }; + final AlertDialog.Builder builder = + new AlertDialog.Builder(context, AlertDialog.THEME_HOLO_DARK); + builder.setMessage(R.string.important_notice_contents) + .setPositiveButton(android.R.string.ok, listener); + showOptionDialog(builder.create(), false /* cancelable */); + } + public void displaySettingsDialog() { if (isShowingOptionDialog()) return; showSubtypeSelectorAndSettings(); @@ -1401,10 +1423,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // because it may differ from mWordComposer.mTypedWord. autoCorrection = sourceSuggestedWords.mTypedWord; } - if (SuggestedWords.EMPTY != suggestedWords) { + if (SuggestedWords.EMPTY == suggestedWords) { + setNeutralSuggestionStrip(); + } else { mInputLogic.mWordComposer.setAutoCorrection(autoCorrection); + setSuggestedWords(suggestedWords, isSuggestionsStripVisible()); } - setSuggestedWords(suggestedWords, isSuggestionsStripVisible()); // Cache the auto-correction in accessibility code so we can speak it if the user // touches a key that will insert it. AccessibilityUtils.getInstance().setAutoCorrection(suggestedWords, @@ -1685,17 +1709,18 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen }; final AlertDialog.Builder builder = new AlertDialog.Builder(this).setItems(items, listener).setTitle(title); - showOptionDialog(builder.create()); + showOptionDialog(builder.create(), true /*cancelable */); } - public void showOptionDialog(final AlertDialog dialog) { + // TODO: Move this method out of {@link LatinIME}. + private void showOptionDialog(final AlertDialog dialog, final boolean cancelable) { final IBinder windowToken = mKeyboardSwitcher.getMainKeyboardView().getWindowToken(); if (windowToken == null) { return; } - dialog.setCancelable(true); - dialog.setCanceledOnTouchOutside(true); + dialog.setCancelable(cancelable); + dialog.setCanceledOnTouchOutside(cancelable); final Window window = dialog.getWindow(); final WindowManager.LayoutParams lp = window.getAttributes(); diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java index 1bc67b2a0..f53183f37 100644 --- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java +++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java @@ -1142,6 +1142,8 @@ public final class InputLogic { // Recorrection is not supported in languages without spaces because we don't know // how to segment them yet. if (!settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces) return; + // If no suggestions are requested, don't try restarting suggestions. + if (!settingsValues.isSuggestionsRequested()) return; // If the cursor is not touching a word, or if there is a selection, return right away. if (mConnection.hasSelection()) return; // If we don't know the cursor location, return. diff --git a/java/src/com/android/inputmethod/latin/makedict/AbstractDictDecoder.java b/java/src/com/android/inputmethod/latin/makedict/AbstractDictDecoder.java index 370782b33..1a9118147 100644 --- a/java/src/com/android/inputmethod/latin/makedict/AbstractDictDecoder.java +++ b/java/src/com/android/inputmethod/latin/makedict/AbstractDictDecoder.java @@ -60,7 +60,7 @@ public abstract class AbstractDictDecoder implements DictDecoder { final FileHeader header = new FileHeader(headerSize, new FusionDictionary.DictionaryOptions(attributes), new FormatOptions(version, FileHeader.ATTRIBUTE_VALUE_TRUE.equals( - attributes.get(FileHeader.HAS_HISTORICAL_INFO_ATTRIBUTE)))); + attributes.get(FileHeader.HAS_HISTORICAL_INFO_KEY)))); return header; } diff --git a/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java index 61c17fc46..74e305976 100644 --- a/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java +++ b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java @@ -336,16 +336,19 @@ public final class FormatSpec { public final int mBodyOffset; public final DictionaryOptions mDictionaryOptions; public final FormatOptions mFormatOptions; + // Note that these are corresponding definitions in native code in latinime::HeaderPolicy // and latinime::HeaderReadWriteUtils. - public static final String USES_FORGETTING_CURVE_ATTRIBUTE = "USES_FORGETTING_CURVE"; - public static final String HAS_HISTORICAL_INFO_ATTRIBUTE = "HAS_HISTORICAL_INFO"; + // TODO: Standardize the key names and bump up the format version, taking care not to + // break format version 2 dictionaries. + public static final String DICTIONARY_VERSION_KEY = "version"; + public static final String DICTIONARY_LOCALE_KEY = "locale"; + public static final String DICTIONARY_ID_KEY = "dictionary"; + public static final String DICTIONARY_DESCRIPTION_KEY = "description"; + public static final String DICTIONARY_DATE_KEY = "date"; + public static final String HAS_HISTORICAL_INFO_KEY = "HAS_HISTORICAL_INFO"; + public static final String USES_FORGETTING_CURVE_KEY = "USES_FORGETTING_CURVE"; public static final String ATTRIBUTE_VALUE_TRUE = "1"; - - public static final String DICTIONARY_VERSION_ATTRIBUTE = "version"; - public static final String DICTIONARY_LOCALE_ATTRIBUTE = "locale"; - public static final String DICTIONARY_ID_ATTRIBUTE = "dictionary"; - private static final String DICTIONARY_DESCRIPTION_ATTRIBUTE = "description"; public FileHeader(final int headerSize, final DictionaryOptions dictionaryOptions, final FormatOptions formatOptions) throws UnsupportedFormatException { mDictionaryOptions = dictionaryOptions; @@ -365,24 +368,24 @@ public final class FormatSpec { // Helper method to get the locale as a String public String getLocaleString() { - return mDictionaryOptions.mAttributes.get(FileHeader.DICTIONARY_LOCALE_ATTRIBUTE); + return mDictionaryOptions.mAttributes.get(FileHeader.DICTIONARY_LOCALE_KEY); } // Helper method to get the version String public String getVersion() { - return mDictionaryOptions.mAttributes.get(FileHeader.DICTIONARY_VERSION_ATTRIBUTE); + return mDictionaryOptions.mAttributes.get(FileHeader.DICTIONARY_VERSION_KEY); } // Helper method to get the dictionary ID as a String public String getId() { - return mDictionaryOptions.mAttributes.get(FileHeader.DICTIONARY_ID_ATTRIBUTE); + return mDictionaryOptions.mAttributes.get(FileHeader.DICTIONARY_ID_KEY); } // Helper method to get the description public String getDescription() { // TODO: Right now each dictionary file comes with a description in its own language. // It will display as is no matter the device's locale. It should be internationalized. - return mDictionaryOptions.mAttributes.get(FileHeader.DICTIONARY_DESCRIPTION_ATTRIBUTE); + return mDictionaryOptions.mAttributes.get(FileHeader.DICTIONARY_DESCRIPTION_KEY); } } diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java b/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java index d34aa171e..b12f79b07 100644 --- a/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java +++ b/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java @@ -62,7 +62,7 @@ public class Ver4DictEncoder implements DictEncoder { final BinaryDictionary binaryDict = new BinaryDictionary(mDictPlacedDir.getAbsolutePath(), 0l, mDictPlacedDir.length(), true /* useFullEditDistance */, LocaleUtils.constructLocaleFromString(dict.mOptions.mAttributes.get( - FormatSpec.FileHeader.DICTIONARY_LOCALE_ATTRIBUTE)), + FormatSpec.FileHeader.DICTIONARY_LOCALE_KEY)), Dictionary.TYPE_USER /* Dictionary type. Does not matter for us */, true /* isUpdatable */); if (!binaryDict.isValidDictionary()) { diff --git a/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java b/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java index 4a610e6f9..d636a253a 100644 --- a/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java +++ b/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java @@ -95,13 +95,13 @@ public abstract class DecayingExpandableBinaryDictionaryBase extends ExpandableB @Override protected Map<String, String> getHeaderAttributeMap() { HashMap<String, String> attributeMap = new HashMap<String, String>(); - attributeMap.put(FormatSpec.FileHeader.USES_FORGETTING_CURVE_ATTRIBUTE, + attributeMap.put(FormatSpec.FileHeader.USES_FORGETTING_CURVE_KEY, FormatSpec.FileHeader.ATTRIBUTE_VALUE_TRUE); - attributeMap.put(FormatSpec.FileHeader.HAS_HISTORICAL_INFO_ATTRIBUTE, + attributeMap.put(FormatSpec.FileHeader.HAS_HISTORICAL_INFO_KEY, FormatSpec.FileHeader.ATTRIBUTE_VALUE_TRUE); - attributeMap.put(FormatSpec.FileHeader.DICTIONARY_ID_ATTRIBUTE, mDictName); - attributeMap.put(FormatSpec.FileHeader.DICTIONARY_LOCALE_ATTRIBUTE, mLocale.toString()); - attributeMap.put(FormatSpec.FileHeader.DICTIONARY_VERSION_ATTRIBUTE, + attributeMap.put(FormatSpec.FileHeader.DICTIONARY_ID_KEY, mDictName); + attributeMap.put(FormatSpec.FileHeader.DICTIONARY_LOCALE_KEY, mLocale.toString()); + attributeMap.put(FormatSpec.FileHeader.DICTIONARY_VERSION_KEY, String.valueOf(TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()))); return attributeMap; } diff --git a/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java b/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java index 8ba32ff76..70cb2b285 100644 --- a/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java +++ b/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java @@ -19,6 +19,7 @@ package com.android.inputmethod.latin.settings; import android.content.res.Resources; import com.android.inputmethod.keyboard.internal.KeySpecParser; +import com.android.inputmethod.keyboard.internal.MoreKeySpec; import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.Dictionary; import com.android.inputmethod.latin.R; @@ -55,7 +56,7 @@ public final class SpacingAndPunctuations { res.getString(R.string.symbols_word_connectors)); mSortedWordSeparators = StringUtils.toSortedCodePointArray( res.getString(R.string.symbols_word_separators)); - final String[] suggestPuncsSpec = KeySpecParser.splitKeySpecs(res.getString( + final String[] suggestPuncsSpec = MoreKeySpec.splitKeySpecs(res.getString( R.string.suggested_punctuations)); mSuggestPuncList = createSuggestPuncList(suggestPuncsSpec); mSentenceSeparator = res.getInteger(R.integer.sentence_separator); diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java index 5ed42ab00..a89f70e1f 100644 --- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java +++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java @@ -28,7 +28,6 @@ import android.graphics.Rect; import android.graphics.Typeface; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; -import android.support.v4.view.GravityCompat; import android.support.v4.view.ViewCompat; import android.text.Spannable; import android.text.SpannableString; @@ -45,6 +44,7 @@ import android.view.ViewGroup; import android.widget.LinearLayout; import android.widget.TextView; +import com.android.inputmethod.compat.TextViewCompatUtils; import com.android.inputmethod.latin.LatinImeLogger; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.SuggestedWords; @@ -492,7 +492,30 @@ final class SuggestionStripLayoutHelper { hintView, 1.0f - mCenterSuggestionWeight, ViewGroup.LayoutParams.MATCH_PARENT); } - private static void setLayoutWeight(final View v, final float weight, final int height) { + public void layoutImportantNotice(final View importantNoticeStrip, final int stripWidth) { + final Resources res = importantNoticeStrip.getResources(); + final Drawable infoIcon = res.getDrawable(R.drawable.sym_keyboard_info_holo_dark); + final Drawable moreIcon = res.getDrawable(R.drawable.sym_keyboard_more_holo_dark); + final int width = stripWidth - infoIcon.getIntrinsicWidth() - moreIcon.getIntrinsicWidth(); + final TextView titleView = (TextView)importantNoticeStrip.findViewById( + R.id.important_notice_title); + titleView.setTextColor(mColorAutoCorrect); + TextViewCompatUtils.setCompoundDrawablesRelativeWithIntrinsicBounds( + titleView, infoIcon, null, moreIcon, null); + final CharSequence importantNoticeTitle = titleView.getText(); + titleView.setTextScaleX(1.0f); // Reset textScaleX. + // When the suggestions strip is displayed first time, stripWidth may be zero. + // Then importantNoticeTitle will be displayed as is without auto text scaleX. + // TODO: Fix the logic to always have a correct value of stripWidth. + if (width > 0) { + // Auto text scaleX to show entire important notice title should be on the strip. + final float titleScaleX = getTextScaleX( + importantNoticeTitle, width, titleView.getPaint()); + titleView.setTextScaleX(titleScaleX); + } + } + + static void setLayoutWeight(final View v, final float weight, final int height) { final ViewGroup.LayoutParams lp = v.getLayoutParams(); if (lp instanceof LinearLayout.LayoutParams) { final LinearLayout.LayoutParams llp = (LinearLayout.LayoutParams)lp; diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java index 22804fce1..b04a2cb5b 100644 --- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java +++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java @@ -54,6 +54,7 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick OnLongClickListener { public interface Listener { public void addWordToUserDictionary(String word); + public void showImportantNoticeContents(); public void pickSuggestionManually(int index, SuggestedWordInfo word); } @@ -62,6 +63,7 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick private final ViewGroup mSuggestionsStrip; private final ViewGroup mAddToDictionaryStrip; + private final View mImportantNoticeStrip; MainKeyboardView mMainKeyboardView; private final View mMoreSuggestionsContainer; @@ -81,10 +83,13 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick private static class StripVisibilityGroup { private final View mSuggestionsStrip; private final View mAddToDictionaryStrip; + private final View mImportantNoticeStrip; - public StripVisibilityGroup(final View suggestionsStrip, final View addToDictionaryStrip) { + public StripVisibilityGroup(final View suggestionsStrip, final View addToDictionaryStrip, + final View importantNoticeStrip) { mSuggestionsStrip = suggestionsStrip; mAddToDictionaryStrip = addToDictionaryStrip; + mImportantNoticeStrip = importantNoticeStrip; showSuggestionsStrip(); } @@ -93,16 +98,25 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick : ViewCompat.LAYOUT_DIRECTION_LTR; ViewCompat.setLayoutDirection(mSuggestionsStrip, layoutDirection); ViewCompat.setLayoutDirection(mAddToDictionaryStrip, layoutDirection); + ViewCompat.setLayoutDirection(mImportantNoticeStrip, layoutDirection); } public void showSuggestionsStrip() { mSuggestionsStrip.setVisibility(VISIBLE); mAddToDictionaryStrip.setVisibility(INVISIBLE); + mImportantNoticeStrip.setVisibility(INVISIBLE); } public void showAddToDictionaryStrip() { mSuggestionsStrip.setVisibility(INVISIBLE); mAddToDictionaryStrip.setVisibility(VISIBLE); + mImportantNoticeStrip.setVisibility(INVISIBLE); + } + + public void showImportantNoticeStrip() { + mSuggestionsStrip.setVisibility(INVISIBLE); + mAddToDictionaryStrip.setVisibility(INVISIBLE); + mImportantNoticeStrip.setVisibility(VISIBLE); } public boolean isShowingAddToDictionaryStrip() { @@ -128,7 +142,9 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick mSuggestionsStrip = (ViewGroup)findViewById(R.id.suggestions_strip); mAddToDictionaryStrip = (ViewGroup)findViewById(R.id.add_to_dictionary_strip); - mStripVisibilityGroup = new StripVisibilityGroup(mSuggestionsStrip, mAddToDictionaryStrip); + mImportantNoticeStrip = findViewById(R.id.important_notice_strip); + mStripVisibilityGroup = new StripVisibilityGroup(mSuggestionsStrip, mAddToDictionaryStrip, + mImportantNoticeStrip); for (int pos = 0; pos < SuggestedWords.MAX_SUGGESTIONS; pos++) { final TextView word = new TextView(context, null, R.attr.suggestionWordStyle); @@ -176,6 +192,7 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { ResearchLogger.suggestionStripView_setSuggestions(mSuggestedWords); } + mStripVisibilityGroup.showSuggestionsStrip(); } public int setMoreSuggestionsHeight(final int remainingHeight) { @@ -203,6 +220,13 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick return false; } + // TODO: This method should be called after this View has been attached and displayed. + public void showImportantNoticeTitle() { + mLayoutHelper.layoutImportantNotice(mImportantNoticeStrip, getWidth()); + mStripVisibilityGroup.showImportantNoticeStrip(); + mImportantNoticeStrip.setOnClickListener(this); + } + public void clear() { mSuggestionsStrip.removeAllViews(); removeAllDebugInfoViews(); @@ -360,6 +384,10 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick @Override public void onClick(final View view) { + if (view == mImportantNoticeStrip) { + mListener.showImportantNoticeContents(); + return; + } final Object tag = view.getTag(); // {@link String} tag is set at {@link #showAddToDictionaryHint(String,CharSequence)}. if (tag instanceof String) { diff --git a/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java b/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java index cc25102ce..bbfa0f091 100644 --- a/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java @@ -102,4 +102,19 @@ public final class CollectionUtils { public static <E> SparseArray<E> newSparseArray() { return new SparseArray<E>(); } + + public static <E> ArrayList<E> arrayAsList(final E[] array, final int start, final int end) { + if (array == null) { + throw new NullPointerException(); + } + if (start < 0 || start > end || end > array.length) { + throw new IllegalArgumentException(); + } + + final ArrayList<E> list = newArrayList(end - start); + for (int i = start; i < end; i++) { + list.add(array[i]); + } + return list; + } } diff --git a/java/src/com/android/inputmethod/latin/utils/CsvUtils.java b/java/src/com/android/inputmethod/latin/utils/CsvUtils.java index 36b927eea..b18a1d83b 100644 --- a/java/src/com/android/inputmethod/latin/utils/CsvUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/CsvUtils.java @@ -17,6 +17,7 @@ package com.android.inputmethod.latin.utils; import com.android.inputmethod.annotations.UsedForTesting; +import com.android.inputmethod.latin.Constants; import java.util.ArrayList; @@ -57,9 +58,9 @@ public final class CsvUtils { // Note that none of these characters match high or low surrogate characters, so we need not // take care of matching by code point. - private static final char COMMA = ','; - private static final char SPACE = ' '; - private static final char QUOTE = '"'; + private static final char COMMA = Constants.CODE_COMMA; + private static final char SPACE = Constants.CODE_SPACE; + private static final char QUOTE = Constants.CODE_DOUBLE_QUOTE; @SuppressWarnings("serial") public static class CsvParseException extends RuntimeException { diff --git a/java/src/com/android/inputmethod/latin/utils/ImportantNoticeUtils.java b/java/src/com/android/inputmethod/latin/utils/ImportantNoticeUtils.java index 8eff2e6e7..4a0823155 100644 --- a/java/src/com/android/inputmethod/latin/utils/ImportantNoticeUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/ImportantNoticeUtils.java @@ -22,13 +22,11 @@ import android.provider.Settings; import android.provider.Settings.SettingNotFoundException; import android.util.Log; +import com.android.inputmethod.latin.R; + public final class ImportantNoticeUtils { private static final String TAG = ImportantNoticeUtils.class.getSimpleName(); - // The current version number of an important notice. - // This number must be incremented whenever users should see a new important notice. - private static final int CURRENT_IMPORTANT_NOTICE_VERSION = 0; - // {@link SharedPreferences} name to save the last important notice version that has been // displayed to users. private static final String PREFERENCE_NAME = "important_notice"; @@ -60,16 +58,20 @@ public final class ImportantNoticeUtils { return context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); } + private static int getCurrentImportantNoticeVersion(final Context context) { + return context.getResources().getInteger(R.integer.config_important_notice_version); + } + public static boolean hasNewImportantNotice(final Context context) { final SharedPreferences prefs = getImportantNoticePreferences(context); final int lastVersion = prefs.getInt(KEY_IMPORTANT_NOTICE_VERSION, 0); - return CURRENT_IMPORTANT_NOTICE_VERSION > lastVersion; + return getCurrentImportantNoticeVersion(context) > lastVersion; } public static void updateLastImportantNoticeVersion(final Context context) { final SharedPreferences prefs = getImportantNoticePreferences(context); prefs.edit() - .putInt(KEY_IMPORTANT_NOTICE_VERSION, CURRENT_IMPORTANT_NOTICE_VERSION) + .putInt(KEY_IMPORTANT_NOTICE_VERSION, getCurrentImportantNoticeVersion(context)) .apply(); } } diff --git a/java/src/com/android/inputmethod/latin/utils/StringUtils.java b/java/src/com/android/inputmethod/latin/utils/StringUtils.java index b154623ae..c632a71a9 100644 --- a/java/src/com/android/inputmethod/latin/utils/StringUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/StringUtils.java @@ -16,6 +16,8 @@ package com.android.inputmethod.latin.utils; +import static com.android.inputmethod.latin.Constants.CODE_UNSPECIFIED; + import android.text.TextUtils; import com.android.inputmethod.annotations.UsedForTesting; @@ -471,4 +473,20 @@ public final class StringUtils { } return bytes; } + + public static String toUpperCaseOfStringForLocale(final String text, + final boolean needsToUpperCase, final Locale locale) { + if (text == null || !needsToUpperCase) return text; + return text.toUpperCase(locale); + } + + public static int toUpperCaseOfCodeForLocale(final int code, final boolean needsToUpperCase, + final Locale locale) { + if (!Constants.isLetterCode(code) || !needsToUpperCase) return code; + final String text = newSingleCodePointString(code); + final String casedText = toUpperCaseOfStringForLocale( + text, needsToUpperCase, locale); + return codePointCount(casedText) == 1 + ? casedText.codePointAt(0) : CODE_UNSPECIFIED; + } } diff --git a/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java b/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java index db628fe18..7af03da59 100644 --- a/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java @@ -22,6 +22,7 @@ import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.latin.makedict.BinaryDictIOUtils; import com.android.inputmethod.latin.makedict.DictDecoder; import com.android.inputmethod.latin.makedict.DictEncoder; +import com.android.inputmethod.latin.makedict.FormatSpec; import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions; import com.android.inputmethod.latin.makedict.FusionDictionary; import com.android.inputmethod.latin.makedict.FusionDictionary.PtNodeArray; @@ -44,9 +45,6 @@ import java.util.concurrent.TimeUnit; public final class UserHistoryDictIOUtils { private static final String TAG = UserHistoryDictIOUtils.class.getSimpleName(); private static final boolean DEBUG = false; - private static final String USES_FORGETTING_CURVE_KEY = "USES_FORGETTING_CURVE"; - private static final String USES_FORGETTING_CURVE_VALUE = "1"; - private static final String LAST_UPDATED_TIME_KEY = "date"; public interface OnAddWordListener { /** @@ -75,8 +73,9 @@ public final class UserHistoryDictIOUtils { final BigramDictionaryInterface dict, final UserHistoryDictionaryBigramList bigrams, final FormatOptions formatOptions, final HashMap<String, String> options) { final FusionDictionary fusionDict = constructFusionDictionary(dict, bigrams, options); - fusionDict.addOptionAttribute(USES_FORGETTING_CURVE_KEY, USES_FORGETTING_CURVE_VALUE); - fusionDict.addOptionAttribute(LAST_UPDATED_TIME_KEY, + fusionDict.addOptionAttribute(FormatSpec.FileHeader.USES_FORGETTING_CURVE_KEY, + FormatSpec.FileHeader.ATTRIBUTE_VALUE_TRUE); + fusionDict.addOptionAttribute(FormatSpec.FileHeader.DICTIONARY_DATE_KEY, String.valueOf(TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()))); try { dictEncoder.writeDictionary(fusionDict, formatOptions); diff --git a/java/src/com/android/inputmethod/latin/utils/UnigramProperty.java b/java/src/com/android/inputmethod/latin/utils/WordProperty.java index 4feee4393..ba9b114b0 100644 --- a/java/src/com/android/inputmethod/latin/utils/UnigramProperty.java +++ b/java/src/com/android/inputmethod/latin/utils/WordProperty.java @@ -26,21 +26,36 @@ import java.util.ArrayList; // This has information that belong to a unigram. This class has some detailed attributes such as // historical information but they have to be checked only for testing purpose. @UsedForTesting -public class UnigramProperty { +public class WordProperty { public final String mCodePoints; public final boolean mIsNotAWord; public final boolean mIsBlacklisted; public final boolean mHasBigrams; public final boolean mHasShortcuts; - public final int mProbability; - // mTimestamp, mLevel and mCount are historical info. These values are depend on the - // implementation in native code; thus, we must not use them and have any assumptions about - // them except for tests. - public final int mTimestamp; - public final int mLevel; - public final int mCount; + public final ProbabilityInfo mProbabilityInfo; + public final ArrayList<WeightedString> mBigramTargets = CollectionUtils.newArrayList(); + public final ArrayList<ProbabilityInfo> mBigramProbabilityInfo = CollectionUtils.newArrayList(); public final ArrayList<WeightedString> mShortcutTargets = CollectionUtils.newArrayList(); + // TODO: Use this kind of Probability class for dictionary read/write code under the makedict + // package. + public static final class ProbabilityInfo { + public final int mProbability; + // wTimestamp, mLevel and mCount are historical info. These values are depend on the + // implementation in native code; thus, we must not use them and have any assumptions about + // them except for tests. + public final int mTimestamp; + public final int mLevel; + public final int mCount; + + public ProbabilityInfo(final int[] probabilityInfo) { + mProbability = probabilityInfo[BinaryDictionary.FORMAT_WORD_PROPERTY_PROBABILITY_INDEX]; + mTimestamp = probabilityInfo[BinaryDictionary.FORMAT_WORD_PROPERTY_TIMESTAMP_INDEX]; + mLevel = probabilityInfo[BinaryDictionary.FORMAT_WORD_PROPERTY_LEVEL_INDEX]; + mCount = probabilityInfo[BinaryDictionary.FORMAT_WORD_PROPERTY_COUNT_INDEX]; + } + } + private static int getCodePointCount(final int[] codePoints) { for (int i = 0; i < codePoints.length; i++) { if (codePoints[i] == 0) { @@ -50,21 +65,32 @@ public class UnigramProperty { return codePoints.length; } - // This represents invalid unigram when the probability is BinaryDictionary.NOT_A_PROBABILITY. - public UnigramProperty(final int[] codePoints, final boolean isNotAWord, + // This represents invalid word when the probability is BinaryDictionary.NOT_A_PROBABILITY. + public WordProperty(final int[] codePoints, final boolean isNotAWord, final boolean isBlacklisted, final boolean hasBigram, - final boolean hasShortcuts, final int probability, final int timestamp, - final int level, final int count, final ArrayList<int[]> shortcutTargets, + final boolean hasShortcuts, final int[] probabilityInfo, + final ArrayList<int[]> bigramTargets, final ArrayList<int[]> bigramProbabilityInfo, + final ArrayList<int[]> shortcutTargets, final ArrayList<Integer> shortcutProbabilities) { mCodePoints = new String(codePoints, 0 /* offset */, getCodePointCount(codePoints)); mIsNotAWord = isNotAWord; mIsBlacklisted = isBlacklisted; mHasBigrams = hasBigram; mHasShortcuts = hasShortcuts; - mProbability = probability; - mTimestamp = timestamp; - mLevel = level; - mCount = count; + mProbabilityInfo = new ProbabilityInfo(probabilityInfo); + + final int bigramTargetCount = bigramTargets.size(); + for (int i = 0; i < bigramTargetCount; i++) { + final int[] bigramTargetCodePointArray = bigramTargets.get(i); + final String bigramTargetString = new String(bigramTargetCodePointArray, + 0 /* offset */, getCodePointCount(bigramTargetCodePointArray)); + final ProbabilityInfo bigramProbability = + new ProbabilityInfo(bigramProbabilityInfo.get(i)); + mBigramTargets.add( + new WeightedString(bigramTargetString, bigramProbability.mProbability)); + mBigramProbabilityInfo.add(bigramProbability); + } + final int shortcutTargetCount = shortcutTargets.size(); for (int i = 0; i < shortcutTargetCount; i++) { final int[] shortcutTargetCodePointArray = shortcutTargets.get(i); @@ -77,6 +103,6 @@ public class UnigramProperty { @UsedForTesting public boolean isValid() { - return mProbability != BinaryDictionary.NOT_A_PROBABILITY; + return mProbabilityInfo.mProbability != BinaryDictionary.NOT_A_PROBABILITY; } }
\ No newline at end of file |