aboutsummaryrefslogtreecommitdiffstats
path: root/common/src/com/android/inputmethod
diff options
context:
space:
mode:
Diffstat (limited to 'common/src/com/android/inputmethod')
-rw-r--r--common/src/com/android/inputmethod/annotations/ExternallyReferenced.java24
-rw-r--r--common/src/com/android/inputmethod/annotations/UsedForTesting.java24
-rw-r--r--common/src/com/android/inputmethod/latin/common/CodePointUtils.java117
-rw-r--r--common/src/com/android/inputmethod/latin/common/CollectionUtils.java63
-rw-r--r--common/src/com/android/inputmethod/latin/common/ComposedData.java66
-rw-r--r--common/src/com/android/inputmethod/latin/common/Constants.java336
-rw-r--r--common/src/com/android/inputmethod/latin/common/CoordinateUtils.java94
-rw-r--r--common/src/com/android/inputmethod/latin/common/FileUtils.java54
-rw-r--r--common/src/com/android/inputmethod/latin/common/InputPointers.java165
-rw-r--r--common/src/com/android/inputmethod/latin/common/LocaleUtils.java210
-rw-r--r--common/src/com/android/inputmethod/latin/common/NativeSuggestOptions.java76
-rw-r--r--common/src/com/android/inputmethod/latin/common/ResizableIntArray.java159
-rw-r--r--common/src/com/android/inputmethod/latin/common/StringUtils.java704
13 files changed, 2092 insertions, 0 deletions
diff --git a/common/src/com/android/inputmethod/annotations/ExternallyReferenced.java b/common/src/com/android/inputmethod/annotations/ExternallyReferenced.java
new file mode 100644
index 000000000..ea5f12ce2
--- /dev/null
+++ b/common/src/com/android/inputmethod/annotations/ExternallyReferenced.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.annotations;
+
+/**
+ * Denotes that the class, method or field should not be eliminated by ProGuard,
+ * because it is externally referenced. (See proguard.flags)
+ */
+public @interface ExternallyReferenced {
+}
diff --git a/common/src/com/android/inputmethod/annotations/UsedForTesting.java b/common/src/com/android/inputmethod/annotations/UsedForTesting.java
new file mode 100644
index 000000000..2ada091e4
--- /dev/null
+++ b/common/src/com/android/inputmethod/annotations/UsedForTesting.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.annotations;
+
+/**
+ * Denotes that the class, method or field should not be eliminated by ProGuard,
+ * so that unit tests can access it. (See proguard.flags)
+ */
+public @interface UsedForTesting {
+}
diff --git a/common/src/com/android/inputmethod/latin/common/CodePointUtils.java b/common/src/com/android/inputmethod/latin/common/CodePointUtils.java
new file mode 100644
index 000000000..ec59de850
--- /dev/null
+++ b/common/src/com/android/inputmethod/latin/common/CodePointUtils.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.latin.common;
+
+import com.android.inputmethod.annotations.UsedForTesting;
+
+import java.util.Random;
+
+import javax.annotation.Nonnull;
+
+// Utility methods related with code points used for tests.
+// TODO: Figure out where this class should be.
+@UsedForTesting
+public class CodePointUtils {
+ private CodePointUtils() {
+ // This utility class is not publicly instantiable.
+ }
+
+ public static final int[] LATIN_ALPHABETS_LOWER = {
+ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
+ 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
+ 0x00E0 /* LATIN SMALL LETTER A WITH GRAVE */,
+ 0x00E1 /* LATIN SMALL LETTER A WITH ACUTE */,
+ 0x00E2 /* LATIN SMALL LETTER A WITH CIRCUMFLEX */,
+ 0x00E3 /* LATIN SMALL LETTER A WITH TILDE */,
+ 0x00E4 /* LATIN SMALL LETTER A WITH DIAERESIS */,
+ 0x00E5 /* LATIN SMALL LETTER A WITH RING ABOVE */,
+ 0x00E6 /* LATIN SMALL LETTER AE */,
+ 0x00E7 /* LATIN SMALL LETTER C WITH CEDILLA */,
+ 0x00E8 /* LATIN SMALL LETTER E WITH GRAVE */,
+ 0x00E9 /* LATIN SMALL LETTER E WITH ACUTE */,
+ 0x00EA /* LATIN SMALL LETTER E WITH CIRCUMFLEX */,
+ 0x00EB /* LATIN SMALL LETTER E WITH DIAERESIS */,
+ 0x00EC /* LATIN SMALL LETTER I WITH GRAVE */,
+ 0x00ED /* LATIN SMALL LETTER I WITH ACUTE */,
+ 0x00EE /* LATIN SMALL LETTER I WITH CIRCUMFLEX */,
+ 0x00EF /* LATIN SMALL LETTER I WITH DIAERESIS */,
+ 0x00F0 /* LATIN SMALL LETTER ETH */,
+ 0x00F1 /* LATIN SMALL LETTER N WITH TILDE */,
+ 0x00F2 /* LATIN SMALL LETTER O WITH GRAVE */,
+ 0x00F3 /* LATIN SMALL LETTER O WITH ACUTE */,
+ 0x00F4 /* LATIN SMALL LETTER O WITH CIRCUMFLEX */,
+ 0x00F5 /* LATIN SMALL LETTER O WITH TILDE */,
+ 0x00F6 /* LATIN SMALL LETTER O WITH DIAERESIS */,
+ 0x00F7 /* LATIN SMALL LETTER O WITH STROKE */,
+ 0x00F9 /* LATIN SMALL LETTER U WITH GRAVE */,
+ 0x00FA /* LATIN SMALL LETTER U WITH ACUTE */,
+ 0x00FB /* LATIN SMALL LETTER U WITH CIRCUMFLEX */,
+ 0x00FC /* LATIN SMALL LETTER U WITH DIAERESIS */,
+ 0x00FD /* LATIN SMALL LETTER Y WITH ACUTE */,
+ 0x00FE /* LATIN SMALL LETTER THORN */,
+ 0x00FF /* LATIN SMALL LETTER Y WITH DIAERESIS */
+ };
+
+ @UsedForTesting
+ @Nonnull
+ public static int[] generateCodePointSet(final int codePointSetSize,
+ @Nonnull final Random random) {
+ final int[] codePointSet = new int[codePointSetSize];
+ for (int i = codePointSet.length - 1; i >= 0; ) {
+ final int r = Math.abs(random.nextInt());
+ if (r < 0) {
+ continue;
+ }
+ // Don't insert 0~0x20, but insert any other code point.
+ // Code points are in the range 0~0x10FFFF.
+ final int candidateCodePoint = 0x20 + r % (Character.MAX_CODE_POINT - 0x20);
+ // Code points between MIN_ and MAX_SURROGATE are not valid on their own.
+ if (candidateCodePoint >= Character.MIN_SURROGATE
+ && candidateCodePoint <= Character.MAX_SURROGATE) {
+ continue;
+ }
+ codePointSet[i] = candidateCodePoint;
+ --i;
+ }
+ return codePointSet;
+ }
+
+ /**
+ * Generates a random word.
+ */
+ @UsedForTesting
+ @Nonnull
+ public static String generateWord(@Nonnull final Random random,
+ @Nonnull final int[] codePointSet) {
+ final StringBuilder builder = new StringBuilder();
+ // 8 * 4 = 32 chars max, but we do it the following way so as to bias the random toward
+ // longer words. This should be closer to natural language, and more importantly, it will
+ // exercise the algorithms in dicttool much more.
+ final int count = 1 + (Math.abs(random.nextInt()) % 5)
+ + (Math.abs(random.nextInt()) % 5)
+ + (Math.abs(random.nextInt()) % 5)
+ + (Math.abs(random.nextInt()) % 5)
+ + (Math.abs(random.nextInt()) % 5)
+ + (Math.abs(random.nextInt()) % 5)
+ + (Math.abs(random.nextInt()) % 5)
+ + (Math.abs(random.nextInt()) % 5);
+ while (builder.length() < count) {
+ builder.appendCodePoint(codePointSet[Math.abs(random.nextInt()) % codePointSet.length]);
+ }
+ return builder.toString();
+ }
+}
diff --git a/common/src/com/android/inputmethod/latin/common/CollectionUtils.java b/common/src/com/android/inputmethod/latin/common/CollectionUtils.java
new file mode 100644
index 000000000..f7ba693af
--- /dev/null
+++ b/common/src/com/android/inputmethod/latin/common/CollectionUtils.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.latin.common;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/**
+ * Utility methods for working with collections.
+ */
+public final class CollectionUtils {
+ private CollectionUtils() {
+ // This utility class is not publicly instantiable.
+ }
+
+ /**
+ * Converts a sub-range of the given array to an ArrayList of the appropriate type.
+ * @param array Array to be converted.
+ * @param start First index inclusive to be converted.
+ * @param end Last index exclusive to be converted.
+ * @throws IllegalArgumentException if start or end are out of range or start &gt; end.
+ */
+ @Nonnull
+ public static <E> ArrayList<E> arrayAsList(@Nonnull final E[] array, final int start,
+ final int end) {
+ if (start < 0 || start > end || end > array.length) {
+ throw new IllegalArgumentException("Invalid start: " + start + " end: " + end
+ + " with array.length: " + array.length);
+ }
+
+ final ArrayList<E> list = new ArrayList<>(end - start);
+ for (int i = start; i < end; i++) {
+ list.add(array[i]);
+ }
+ return list;
+ }
+
+ /**
+ * Tests whether c contains no elements, true if c is null or c is empty.
+ * @param c Collection to test.
+ * @return Whether c contains no elements.
+ */
+ public static boolean isNullOrEmpty(@Nullable final Collection<?> c) {
+ return c == null || c.isEmpty();
+ }
+}
diff --git a/common/src/com/android/inputmethod/latin/common/ComposedData.java b/common/src/com/android/inputmethod/latin/common/ComposedData.java
new file mode 100644
index 000000000..7f0966050
--- /dev/null
+++ b/common/src/com/android/inputmethod/latin/common/ComposedData.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.latin.common;
+
+import javax.annotation.Nonnull;
+
+/**
+ * An immutable class that encapsulates a snapshot of word composition data.
+ */
+public class ComposedData {
+ @Nonnull
+ public final InputPointers mInputPointers;
+ public final boolean mIsBatchMode;
+ @Nonnull
+ public final String mTypedWord;
+
+ public ComposedData(@Nonnull final InputPointers inputPointers, final boolean isBatchMode,
+ @Nonnull final String typedWord) {
+ mInputPointers = inputPointers;
+ mIsBatchMode = isBatchMode;
+ mTypedWord = typedWord;
+ }
+
+ /**
+ * Copy the code points in the typed word to a destination array of ints.
+ *
+ * If the array is too small to hold the code points in the typed word, nothing is copied and
+ * -1 is returned.
+ *
+ * @param destination the array of ints.
+ * @return the number of copied code points.
+ */
+ public int copyCodePointsExceptTrailingSingleQuotesAndReturnCodePointCount(
+ @Nonnull final int[] destination) {
+ // lastIndex is exclusive
+ final int lastIndex = mTypedWord.length()
+ - StringUtils.getTrailingSingleQuotesCount(mTypedWord);
+ if (lastIndex <= 0) {
+ // The string is empty or contains only single quotes.
+ return 0;
+ }
+
+ // The following function counts the number of code points in the text range which begins
+ // at index 0 and extends to the character at lastIndex.
+ final int codePointSize = Character.codePointCount(mTypedWord, 0, lastIndex);
+ if (codePointSize > destination.length) {
+ return -1;
+ }
+ return StringUtils.copyCodePointsAndReturnCodePointCount(destination, mTypedWord, 0,
+ lastIndex, true /* downCase */);
+ }
+}
diff --git a/common/src/com/android/inputmethod/latin/common/Constants.java b/common/src/com/android/inputmethod/latin/common/Constants.java
new file mode 100644
index 000000000..a860d3560
--- /dev/null
+++ b/common/src/com/android/inputmethod/latin/common/Constants.java
@@ -0,0 +1,336 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.latin.common;
+
+import com.android.inputmethod.annotations.UsedForTesting;
+
+import javax.annotation.Nonnull;
+
+public final class Constants {
+ public static final class Color {
+ /**
+ * The alpha value for fully opaque.
+ */
+ public final static int ALPHA_OPAQUE = 255;
+ }
+
+ public static final class ImeOption {
+ /**
+ * The private IME option used to indicate that no microphone should be shown for a given
+ * text field. For instance, this is specified by the search dialog when the dialog is
+ * already showing a voice search button.
+ *
+ * @deprecated Use {@link ImeOption#NO_MICROPHONE} with package name prefixed.
+ */
+ @SuppressWarnings("dep-ann")
+ public static final String NO_MICROPHONE_COMPAT = "nm";
+
+ /**
+ * The private IME option used to indicate that no microphone should be shown for a given
+ * text field. For instance, this is specified by the search dialog when the dialog is
+ * already showing a voice search button.
+ */
+ public static final String NO_MICROPHONE = "noMicrophoneKey";
+
+ /**
+ * The private IME option used to indicate that no settings key should be shown for a given
+ * text field.
+ */
+ public static final String NO_SETTINGS_KEY = "noSettingsKey";
+
+ /**
+ * The private IME option used to indicate that the given text field needs ASCII code points
+ * input.
+ *
+ * @deprecated Use EditorInfo#IME_FLAG_FORCE_ASCII.
+ */
+ @SuppressWarnings("dep-ann")
+ public static final String FORCE_ASCII = "forceAscii";
+
+ /**
+ * The private IME option used to suppress the floating gesture preview for a given text
+ * field. This overrides the corresponding keyboard settings preference.
+ * {@link com.android.inputmethod.latin.settings.SettingsValues#mGestureFloatingPreviewTextEnabled}
+ */
+ public static final String NO_FLOATING_GESTURE_PREVIEW = "noGestureFloatingPreview";
+
+ private ImeOption() {
+ // This utility class is not publicly instantiable.
+ }
+ }
+
+ public static final class Subtype {
+ /**
+ * The subtype mode used to indicate that the subtype is a keyboard.
+ */
+ public static final String KEYBOARD_MODE = "keyboard";
+
+ public static final class ExtraValue {
+ /**
+ * The subtype extra value used to indicate that this subtype is capable of
+ * entering ASCII characters.
+ */
+ public static final String ASCII_CAPABLE = "AsciiCapable";
+
+ /**
+ * The subtype extra value used to indicate that this subtype is enabled
+ * when the default subtype is not marked as ascii capable.
+ */
+ public static final String ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE =
+ "EnabledWhenDefaultIsNotAsciiCapable";
+
+ /**
+ * The subtype extra value used to indicate that this subtype is capable of
+ * entering emoji characters.
+ */
+ public static final String EMOJI_CAPABLE = "EmojiCapable";
+
+ /**
+ * The subtype extra value used to indicate that this subtype requires a network
+ * connection to work.
+ */
+ public static final String REQ_NETWORK_CONNECTIVITY = "requireNetworkConnectivity";
+
+ /**
+ * The subtype extra value used to indicate that the display name of this subtype
+ * contains a "%s" for printf-like replacement and it should be replaced by
+ * this extra value.
+ * This extra value is supported on JellyBean and later.
+ */
+ public static final String UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME =
+ "UntranslatableReplacementStringInSubtypeName";
+
+ /**
+ * The subtype extra value used to indicate this subtype keyboard layout set name.
+ * This extra value is private to LatinIME.
+ */
+ public static final String KEYBOARD_LAYOUT_SET = "KeyboardLayoutSet";
+
+ /**
+ * The subtype extra value used to indicate that this subtype is an additional subtype
+ * that the user defined. This extra value is private to LatinIME.
+ */
+ public static final String IS_ADDITIONAL_SUBTYPE = "isAdditionalSubtype";
+
+ /**
+ * The subtype extra value used to specify the combining rules.
+ */
+ public static final String COMBINING_RULES = "CombiningRules";
+
+ private ExtraValue() {
+ // This utility class is not publicly instantiable.
+ }
+ }
+
+ private Subtype() {
+ // This utility class is not publicly instantiable.
+ }
+ }
+
+ public static final class TextUtils {
+ /**
+ * Capitalization mode for {@link android.text.TextUtils#getCapsMode}: don't capitalize
+ * characters. This value may be used with
+ * {@link android.text.TextUtils#CAP_MODE_CHARACTERS},
+ * {@link android.text.TextUtils#CAP_MODE_WORDS}, and
+ * {@link android.text.TextUtils#CAP_MODE_SENTENCES}.
+ */
+ // TODO: Straighten this out. It's bizarre to have to use android.text.TextUtils.CAP_MODE_*
+ // except for OFF that is in Constants.TextUtils.
+ public static final int CAP_MODE_OFF = 0;
+
+ private TextUtils() {
+ // This utility class is not publicly instantiable.
+ }
+ }
+
+ public static final int NOT_A_CODE = -1;
+ public static final int NOT_A_CURSOR_POSITION = -1;
+ // TODO: replace the following constants with state in InputTransaction?
+ public static final int NOT_A_COORDINATE = -1;
+ public static final int SUGGESTION_STRIP_COORDINATE = -2;
+ public static final int SPELL_CHECKER_COORDINATE = -3;
+ public static final int EXTERNAL_KEYBOARD_COORDINATE = -4;
+
+ // A hint on how many characters to cache from the TextView. A good value of this is given by
+ // how many characters we need to be able to almost always find the caps mode.
+ public static final int EDITOR_CONTENTS_CACHE_SIZE = 1024;
+ // How many characters we accept for the recapitalization functionality. This needs to be
+ // large enough for all reasonable purposes, but avoid purposeful attacks. 100k sounds about
+ // right for this.
+ public static final int MAX_CHARACTERS_FOR_RECAPITALIZATION = 1024 * 100;
+
+ // Must be equal to MAX_WORD_LENGTH in native/jni/src/defines.h
+ public static final int DICTIONARY_MAX_WORD_LENGTH = 48;
+
+ // (MAX_PREV_WORD_COUNT_FOR_N_GRAM + 1)-gram is supported in Java side. Needs to modify
+ // MAX_PREV_WORD_COUNT_FOR_N_GRAM in native/jni/src/defines.h for suggestions.
+ public static final int MAX_PREV_WORD_COUNT_FOR_N_GRAM = 3;
+
+ // Key events coming any faster than this are long-presses.
+ public static final int LONG_PRESS_MILLISECONDS = 200;
+ // TODO: Set this value appropriately.
+ public static final int GET_SUGGESTED_WORDS_TIMEOUT = 200;
+ // How many continuous deletes at which to start deleting at a higher speed.
+ public static final int DELETE_ACCELERATE_AT = 20;
+
+ public static final String WORD_SEPARATOR = " ";
+
+ public static boolean isValidCoordinate(final int coordinate) {
+ // Detect {@link NOT_A_COORDINATE}, {@link SUGGESTION_STRIP_COORDINATE},
+ // and {@link SPELL_CHECKER_COORDINATE}.
+ return coordinate >= 0;
+ }
+
+ /**
+ * Custom request code used in
+ * {@link com.android.inputmethod.keyboard.KeyboardActionListener#onCustomRequest(int)}.
+ */
+ // The code to show input method picker.
+ public static final int CUSTOM_CODE_SHOW_INPUT_METHOD_PICKER = 1;
+
+ /**
+ * Some common keys code. Must be positive.
+ */
+ public static final int CODE_ENTER = '\n';
+ public static final int CODE_TAB = '\t';
+ public static final int CODE_SPACE = ' ';
+ public static final int CODE_PERIOD = '.';
+ public static final int CODE_COMMA = ',';
+ public static final int CODE_DASH = '-';
+ public static final int CODE_SINGLE_QUOTE = '\'';
+ public static final int CODE_DOUBLE_QUOTE = '"';
+ 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 = '%';
+ public static final int CODE_CLOSING_PARENTHESIS = ')';
+ public static final int CODE_CLOSING_SQUARE_BRACKET = ']';
+ public static final int CODE_CLOSING_CURLY_BRACKET = '}';
+ public static final int CODE_CLOSING_ANGLE_BRACKET = '>';
+ public static final int CODE_INVERTED_QUESTION_MARK = 0xBF; // ¿
+ public static final int CODE_INVERTED_EXCLAMATION_MARK = 0xA1; // ¡
+ public static final int CODE_GRAVE_ACCENT = '`';
+ public static final int CODE_CIRCUMFLEX_ACCENT = '^';
+ public static final int CODE_TILDE = '~';
+
+ public static final String REGEXP_PERIOD = "\\.";
+ public static final String STRING_SPACE = " ";
+
+ /**
+ * Special keys code. Must be negative.
+ * These should be aligned with constants in
+ * {@link com.android.inputmethod.keyboard.internal.KeyboardCodesSet}.
+ */
+ public static final int CODE_SHIFT = -1;
+ public static final int CODE_CAPSLOCK = -2;
+ public static final int CODE_SWITCH_ALPHA_SYMBOL = -3;
+ public static final int CODE_OUTPUT_TEXT = -4;
+ public static final int CODE_DELETE = -5;
+ public static final int CODE_SETTINGS = -6;
+ public static final int CODE_SHORTCUT = -7;
+ public static final int CODE_ACTION_NEXT = -8;
+ public static final int CODE_ACTION_PREVIOUS = -9;
+ public static final int CODE_LANGUAGE_SWITCH = -10;
+ public static final int CODE_EMOJI = -11;
+ public static final int CODE_SHIFT_ENTER = -12;
+ public static final int CODE_SYMBOL_SHIFT = -13;
+ public static final int CODE_ALPHA_FROM_EMOJI = -14;
+ // Code value representing the code is not specified.
+ public static final int CODE_UNSPECIFIED = -15;
+
+ public static boolean isLetterCode(final int code) {
+ return code >= CODE_SPACE;
+ }
+
+ @Nonnull
+ public static String printableCode(final int code) {
+ switch (code) {
+ case CODE_SHIFT: return "shift";
+ case CODE_CAPSLOCK: return "capslock";
+ case CODE_SWITCH_ALPHA_SYMBOL: return "symbol";
+ case CODE_OUTPUT_TEXT: return "text";
+ case CODE_DELETE: return "delete";
+ case CODE_SETTINGS: return "settings";
+ case CODE_SHORTCUT: return "shortcut";
+ case CODE_ACTION_NEXT: return "actionNext";
+ case CODE_ACTION_PREVIOUS: return "actionPrevious";
+ case CODE_LANGUAGE_SWITCH: return "languageSwitch";
+ case CODE_EMOJI: return "emoji";
+ case CODE_SHIFT_ENTER: return "shiftEnter";
+ case CODE_ALPHA_FROM_EMOJI: return "alpha";
+ case CODE_UNSPECIFIED: return "unspec";
+ case CODE_TAB: return "tab";
+ case CODE_ENTER: return "enter";
+ case CODE_SPACE: return "space";
+ default:
+ if (code < CODE_SPACE) return String.format("\\u%02X", code);
+ if (code < 0x100) return String.format("%c", code);
+ if (code < 0x10000) return String.format("\\u%04X", code);
+ return String.format("\\U%05X", code);
+ }
+ }
+
+ @Nonnull
+ public static String printableCodes(@Nonnull final int[] codes) {
+ final StringBuilder sb = new StringBuilder();
+ boolean addDelimiter = false;
+ for (final int code : codes) {
+ if (code == NOT_A_CODE) break;
+ if (addDelimiter) sb.append(", ");
+ sb.append(printableCode(code));
+ addDelimiter = true;
+ }
+ return "[" + sb + "]";
+ }
+
+ /**
+ * Screen metrics (a.k.a. Device form factor) constants of
+ * {@link com.android.inputmethod.latin.R.integer#config_screen_metrics}.
+ */
+ public static final int SCREEN_METRICS_SMALL_PHONE = 0;
+ public static final int SCREEN_METRICS_LARGE_PHONE = 1;
+ public static final int SCREEN_METRICS_LARGE_TABLET = 2;
+ public static final int SCREEN_METRICS_SMALL_TABLET = 3;
+
+ @UsedForTesting
+ public static boolean isPhone(final int screenMetrics) {
+ return screenMetrics == SCREEN_METRICS_SMALL_PHONE
+ || screenMetrics == SCREEN_METRICS_LARGE_PHONE;
+ }
+
+ @UsedForTesting
+ public static boolean isTablet(final int screenMetrics) {
+ return screenMetrics == SCREEN_METRICS_SMALL_TABLET
+ || screenMetrics == SCREEN_METRICS_LARGE_TABLET;
+ }
+
+ /**
+ * Default capacity of gesture points container.
+ * This constant is used by {@link com.android.inputmethod.keyboard.internal.BatchInputArbiter}
+ * and etc. to preallocate regions that contain gesture event points.
+ */
+ public static final int DEFAULT_GESTURE_POINTS_CAPACITY = 128;
+
+ private Constants() {
+ // This utility class is not publicly instantiable.
+ }
+}
diff --git a/common/src/com/android/inputmethod/latin/common/CoordinateUtils.java b/common/src/com/android/inputmethod/latin/common/CoordinateUtils.java
new file mode 100644
index 000000000..031662411
--- /dev/null
+++ b/common/src/com/android/inputmethod/latin/common/CoordinateUtils.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.latin.common;
+
+import javax.annotation.Nonnull;
+
+public final class CoordinateUtils {
+ private static final int INDEX_X = 0;
+ private static final int INDEX_Y = 1;
+ private static final int ELEMENT_SIZE = INDEX_Y + 1;
+
+ private CoordinateUtils() {
+ // This utility class is not publicly instantiable.
+ }
+
+ @Nonnull
+ public static int[] newInstance() {
+ return new int[ELEMENT_SIZE];
+ }
+
+ public static int x(@Nonnull final int[] coords) {
+ return coords[INDEX_X];
+ }
+
+ public static int y(@Nonnull final int[] coords) {
+ return coords[INDEX_Y];
+ }
+
+ public static void set(@Nonnull final int[] coords, final int x, final int y) {
+ coords[INDEX_X] = x;
+ coords[INDEX_Y] = y;
+ }
+
+ public static void copy(@Nonnull final int[] destination, @Nonnull final int[] source) {
+ destination[INDEX_X] = source[INDEX_X];
+ destination[INDEX_Y] = source[INDEX_Y];
+ }
+
+ @Nonnull
+ public static int[] newCoordinateArray(final int arraySize) {
+ return new int[ELEMENT_SIZE * arraySize];
+ }
+
+ @Nonnull
+ public static int[] newCoordinateArray(final int arraySize,
+ final int defaultX, final int defaultY) {
+ final int[] result = new int[ELEMENT_SIZE * arraySize];
+ for (int i = 0; i < arraySize; ++i) {
+ setXYInArray(result, i, defaultX, defaultY);
+ }
+ return result;
+ }
+
+ public static int xFromArray(@Nonnull final int[] coordsArray, final int index) {
+ return coordsArray[ELEMENT_SIZE * index + INDEX_X];
+ }
+
+ public static int yFromArray(@Nonnull final int[] coordsArray, final int index) {
+ return coordsArray[ELEMENT_SIZE * index + INDEX_Y];
+ }
+
+ @Nonnull
+ public static int[] coordinateFromArray(@Nonnull final int[] coordsArray, final int index) {
+ final int[] coords = newInstance();
+ set(coords, xFromArray(coordsArray, index), yFromArray(coordsArray, index));
+ return coords;
+ }
+
+ public static void setXYInArray(@Nonnull final int[] coordsArray, final int index,
+ final int x, final int y) {
+ final int baseIndex = ELEMENT_SIZE * index;
+ coordsArray[baseIndex + INDEX_X] = x;
+ coordsArray[baseIndex + INDEX_Y] = y;
+ }
+
+ public static void setCoordinateInArray(@Nonnull final int[] coordsArray, final int index,
+ @Nonnull final int[] coords) {
+ setXYInArray(coordsArray, index, x(coords), y(coords));
+ }
+}
diff --git a/common/src/com/android/inputmethod/latin/common/FileUtils.java b/common/src/com/android/inputmethod/latin/common/FileUtils.java
new file mode 100644
index 000000000..676845842
--- /dev/null
+++ b/common/src/com/android/inputmethod/latin/common/FileUtils.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.inputmethod.latin.common;
+
+import java.io.File;
+import java.io.FilenameFilter;
+
+/**
+ * A simple class to help with removing directories recursively.
+ */
+public class FileUtils {
+ public static boolean deleteRecursively(final File path) {
+ if (path.isDirectory()) {
+ final File[] files = path.listFiles();
+ if (files != null) {
+ for (final File child : files) {
+ deleteRecursively(child);
+ }
+ }
+ }
+ return path.delete();
+ }
+
+ public static boolean deleteFilteredFiles(final File dir, final FilenameFilter fileNameFilter) {
+ if (!dir.isDirectory()) {
+ return false;
+ }
+ final File[] files = dir.listFiles(fileNameFilter);
+ if (files == null) {
+ return false;
+ }
+ boolean hasDeletedAllFiles = true;
+ for (final File file : files) {
+ if (!deleteRecursively(file)) {
+ hasDeletedAllFiles = false;
+ }
+ }
+ return hasDeletedAllFiles;
+ }
+}
diff --git a/common/src/com/android/inputmethod/latin/common/InputPointers.java b/common/src/com/android/inputmethod/latin/common/InputPointers.java
new file mode 100644
index 000000000..7beee1536
--- /dev/null
+++ b/common/src/com/android/inputmethod/latin/common/InputPointers.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.latin.common;
+
+import com.android.inputmethod.annotations.UsedForTesting;
+
+import javax.annotation.Nonnull;
+
+// TODO: This class is not thread-safe.
+public final class InputPointers {
+ private static final boolean DEBUG_TIME = false;
+
+ private final int mDefaultCapacity;
+ private final ResizableIntArray mXCoordinates;
+ private final ResizableIntArray mYCoordinates;
+ private final ResizableIntArray mPointerIds;
+ private final ResizableIntArray mTimes;
+
+ public InputPointers(final int defaultCapacity) {
+ mDefaultCapacity = defaultCapacity;
+ mXCoordinates = new ResizableIntArray(defaultCapacity);
+ mYCoordinates = new ResizableIntArray(defaultCapacity);
+ mPointerIds = new ResizableIntArray(defaultCapacity);
+ mTimes = new ResizableIntArray(defaultCapacity);
+ }
+
+ private void fillWithLastTimeUntil(final int index) {
+ final int fromIndex = mTimes.getLength();
+ // Fill the gap with the latest time.
+ // See {@link #getTime(int)} and {@link #isValidTimeStamps()}.
+ if (fromIndex <= 0) {
+ return;
+ }
+ final int fillLength = index - fromIndex + 1;
+ if (fillLength <= 0) {
+ return;
+ }
+ final int lastTime = mTimes.get(fromIndex - 1);
+ mTimes.fill(lastTime, fromIndex, fillLength);
+ }
+
+ public void addPointerAt(final int index, final int x, final int y, final int pointerId,
+ final int time) {
+ mXCoordinates.addAt(index, x);
+ mYCoordinates.addAt(index, y);
+ mPointerIds.addAt(index, pointerId);
+ if (DEBUG_TIME) {
+ fillWithLastTimeUntil(index);
+ }
+ mTimes.addAt(index, time);
+ }
+
+ @UsedForTesting
+ public void addPointer(final int x, final int y, final int pointerId, final int time) {
+ mXCoordinates.add(x);
+ mYCoordinates.add(y);
+ mPointerIds.add(pointerId);
+ mTimes.add(time);
+ }
+
+ public void set(@Nonnull final InputPointers ip) {
+ mXCoordinates.set(ip.mXCoordinates);
+ mYCoordinates.set(ip.mYCoordinates);
+ mPointerIds.set(ip.mPointerIds);
+ mTimes.set(ip.mTimes);
+ }
+
+ public void copy(@Nonnull final InputPointers ip) {
+ mXCoordinates.copy(ip.mXCoordinates);
+ mYCoordinates.copy(ip.mYCoordinates);
+ mPointerIds.copy(ip.mPointerIds);
+ mTimes.copy(ip.mTimes);
+ }
+
+ /**
+ * Append the times, x-coordinates and y-coordinates in the specified {@link ResizableIntArray}
+ * to the end of this.
+ * @param pointerId the pointer id of the source.
+ * @param times the source {@link ResizableIntArray} to read the event times from.
+ * @param xCoordinates the source {@link ResizableIntArray} to read the x-coordinates from.
+ * @param yCoordinates the source {@link ResizableIntArray} to read the y-coordinates from.
+ * @param startPos the starting index of the data in {@code times} and etc.
+ * @param length the number of data to be appended.
+ */
+ public void append(final int pointerId, @Nonnull final ResizableIntArray times,
+ @Nonnull final ResizableIntArray xCoordinates,
+ @Nonnull final ResizableIntArray yCoordinates, final int startPos, final int length) {
+ if (length == 0) {
+ return;
+ }
+ mXCoordinates.append(xCoordinates, startPos, length);
+ mYCoordinates.append(yCoordinates, startPos, length);
+ mPointerIds.fill(pointerId, mPointerIds.getLength(), length);
+ mTimes.append(times, startPos, length);
+ }
+
+ /**
+ * Shift to the left by elementCount, discarding elementCount pointers at the start.
+ * @param elementCount how many elements to shift.
+ */
+ public void shift(final int elementCount) {
+ mXCoordinates.shift(elementCount);
+ mYCoordinates.shift(elementCount);
+ mPointerIds.shift(elementCount);
+ mTimes.shift(elementCount);
+ }
+
+ public void reset() {
+ final int defaultCapacity = mDefaultCapacity;
+ mXCoordinates.reset(defaultCapacity);
+ mYCoordinates.reset(defaultCapacity);
+ mPointerIds.reset(defaultCapacity);
+ mTimes.reset(defaultCapacity);
+ }
+
+ public int getPointerSize() {
+ return mXCoordinates.getLength();
+ }
+
+ @Nonnull
+ public int[] getXCoordinates() {
+ return mXCoordinates.getPrimitiveArray();
+ }
+
+ @Nonnull
+ public int[] getYCoordinates() {
+ return mYCoordinates.getPrimitiveArray();
+ }
+
+ @Nonnull
+ public int[] getPointerIds() {
+ return mPointerIds.getPrimitiveArray();
+ }
+
+ /**
+ * Gets the time each point was registered, in milliseconds, relative to the first event in the
+ * sequence.
+ * @return The time each point was registered, in milliseconds, relative to the first event in
+ * the sequence.
+ */
+ @Nonnull
+ public int[] getTimes() {
+ return mTimes.getPrimitiveArray();
+ }
+
+ @Override
+ public String toString() {
+ return "size=" + getPointerSize() + " id=" + mPointerIds + " time=" + mTimes
+ + " x=" + mXCoordinates + " y=" + mYCoordinates;
+ }
+}
diff --git a/common/src/com/android/inputmethod/latin/common/LocaleUtils.java b/common/src/com/android/inputmethod/latin/common/LocaleUtils.java
new file mode 100644
index 000000000..d5878c024
--- /dev/null
+++ b/common/src/com/android/inputmethod/latin/common/LocaleUtils.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.inputmethod.latin.common;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Locale;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/**
+ * A class to help with handling Locales in string form.
+ *
+ * This file has the same meaning and features (and shares all of its code) with the one with the
+ * same name in Latin IME. They need to be kept synchronized; for any update/bugfix to
+ * this file, consider also updating/fixing the version in Latin IME.
+ */
+public final class LocaleUtils {
+ private LocaleUtils() {
+ // Intentional empty constructor for utility class.
+ }
+
+ // Locale match level constants.
+ // A higher level of match is guaranteed to have a higher numerical value.
+ // Some room is left within constants to add match cases that may arise necessary
+ // in the future, for example differentiating between the case where the countries
+ // are both present and different, and the case where one of the locales does not
+ // specify the countries. This difference is not needed now.
+
+ // Nothing matches.
+ public static final int LOCALE_NO_MATCH = 0;
+ // The languages matches, but the country are different. Or, the reference locale requires a
+ // country and the tested locale does not have one.
+ public static final int LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER = 3;
+ // The languages and country match, but the variants are different. Or, the reference locale
+ // requires a variant and the tested locale does not have one.
+ public static final int LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER = 6;
+ // The required locale is null or empty so it will accept anything, and the tested locale
+ // is non-null and non-empty.
+ public static final int LOCALE_ANY_MATCH = 10;
+ // The language matches, and the tested locale specifies a country but the reference locale
+ // does not require one.
+ public static final int LOCALE_LANGUAGE_MATCH = 15;
+ // The language and the country match, and the tested locale specifies a variant but the
+ // reference locale does not require one.
+ public static final int LOCALE_LANGUAGE_AND_COUNTRY_MATCH = 20;
+ // The compared locales are fully identical. This is the best match level.
+ public static final int LOCALE_FULL_MATCH = 30;
+
+ // The level at which a match is "normally" considered a locale match with standard algorithms.
+ // Don't use this directly, use #isMatch to test.
+ private static final int LOCALE_MATCH = LOCALE_ANY_MATCH;
+
+ // Make this match the maximum match level. If this evolves to have more than 2 digits
+ // when written in base 10, also adjust the getMatchLevelSortedString method.
+ private static final int MATCH_LEVEL_MAX = 30;
+
+ /**
+ * Return how well a tested locale matches a reference locale.
+ *
+ * This will check the tested locale against the reference locale and return a measure of how
+ * a well it matches the reference. The general idea is that the tested locale has to match
+ * every specified part of the required locale. A full match occur when they are equal, a
+ * partial match when the tested locale agrees with the reference locale but is more specific,
+ * and a difference when the tested locale does not comply with all requirements from the
+ * reference locale.
+ * In more detail, if the reference locale specifies at least a language and the testedLocale
+ * does not specify one, or specifies a different one, LOCALE_NO_MATCH is returned. If the
+ * reference locale is empty or null, it will match anything - in the form of LOCALE_FULL_MATCH
+ * if the tested locale is empty or null, and LOCALE_ANY_MATCH otherwise. If the reference and
+ * tested locale agree on the language, but not on the country,
+ * LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER is returned if the reference locale specifies a country,
+ * and LOCALE_LANGUAGE_MATCH otherwise.
+ * If they agree on both the language and the country, but not on the variant,
+ * LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER is returned if the reference locale
+ * specifies a variant, and LOCALE_LANGUAGE_AND_COUNTRY_MATCH otherwise. If everything matches,
+ * LOCALE_FULL_MATCH is returned.
+ * Examples:
+ * en <=> en_US => LOCALE_LANGUAGE_MATCH
+ * en_US <=> en => LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER
+ * en_US_POSIX <=> en_US_Android => LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER
+ * en_US <=> en_US_Android => LOCALE_LANGUAGE_AND_COUNTRY_MATCH
+ * sp_US <=> en_US => LOCALE_NO_MATCH
+ * de <=> de => LOCALE_FULL_MATCH
+ * en_US <=> en_US => LOCALE_FULL_MATCH
+ * "" <=> en_US => LOCALE_ANY_MATCH
+ *
+ * @param referenceLocale the reference locale to test against.
+ * @param testedLocale the locale to test.
+ * @return a constant that measures how well the tested locale matches the reference locale.
+ */
+ public static int getMatchLevel(@Nullable final String referenceLocale,
+ @Nullable final String testedLocale) {
+ if (StringUtils.isEmpty(referenceLocale)) {
+ return StringUtils.isEmpty(testedLocale) ? LOCALE_FULL_MATCH : LOCALE_ANY_MATCH;
+ }
+ if (null == testedLocale) return LOCALE_NO_MATCH;
+ final String[] referenceParams = referenceLocale.split("_", 3);
+ final String[] testedParams = testedLocale.split("_", 3);
+ // By spec of String#split, [0] cannot be null and length cannot be 0.
+ if (!referenceParams[0].equals(testedParams[0])) return LOCALE_NO_MATCH;
+ switch (referenceParams.length) {
+ case 1:
+ return 1 == testedParams.length ? LOCALE_FULL_MATCH : LOCALE_LANGUAGE_MATCH;
+ case 2:
+ if (1 == testedParams.length) return LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER;
+ if (!referenceParams[1].equals(testedParams[1]))
+ return LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER;
+ if (3 == testedParams.length) return LOCALE_LANGUAGE_AND_COUNTRY_MATCH;
+ return LOCALE_FULL_MATCH;
+ case 3:
+ if (1 == testedParams.length) return LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER;
+ if (!referenceParams[1].equals(testedParams[1]))
+ return LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER;
+ if (2 == testedParams.length) return LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER;
+ if (!referenceParams[2].equals(testedParams[2]))
+ return LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER;
+ return LOCALE_FULL_MATCH;
+ }
+ // It should be impossible to come here
+ return LOCALE_NO_MATCH;
+ }
+
+ /**
+ * Return a string that represents this match level, with better matches first.
+ *
+ * The strings are sorted in lexicographic order: a better match will always be less than
+ * a worse match when compared together.
+ */
+ public static String getMatchLevelSortedString(final int matchLevel) {
+ // This works because the match levels are 0~99 (actually 0~30)
+ // Ideally this should use a number of digits equals to the 1og10 of the greater matchLevel
+ return String.format(Locale.ROOT, "%02d", MATCH_LEVEL_MAX - matchLevel);
+ }
+
+ /**
+ * Find out whether a match level should be considered a match.
+ *
+ * This method takes a match level as returned by the #getMatchLevel method, and returns whether
+ * it should be considered a match in the usual sense with standard Locale functions.
+ *
+ * @param level the match level, as returned by getMatchLevel.
+ * @return whether this is a match or not.
+ */
+ public static boolean isMatch(final int level) {
+ return LOCALE_MATCH <= level;
+ }
+
+ private static final HashMap<String, Locale> sLocaleCache = new HashMap<>();
+
+ /**
+ * Creates a locale from a string specification.
+ * @param localeString a string specification of a locale, in a format of "ll_cc_variant" where
+ * "ll" is a language code, "cc" is a country code.
+ */
+ @Nonnull
+ public static Locale constructLocaleFromString(@Nonnull final String localeString) {
+ synchronized (sLocaleCache) {
+ if (sLocaleCache.containsKey(localeString)) {
+ return sLocaleCache.get(localeString);
+ }
+ final String[] elements = localeString.split("_", 3);
+ final Locale locale;
+ if (elements.length == 1) {
+ locale = new Locale(elements[0] /* language */);
+ } else if (elements.length == 2) {
+ locale = new Locale(elements[0] /* language */, elements[1] /* country */);
+ } else { // localeParams.length == 3
+ locale = new Locale(elements[0] /* language */, elements[1] /* country */,
+ elements[2] /* variant */);
+ }
+ sLocaleCache.put(localeString, locale);
+ return locale;
+ }
+ }
+
+ // TODO: Get this information from the framework instead of maintaining here by ourselves.
+ private static final HashSet<String> sRtlLanguageCodes = new HashSet<>();
+ static {
+ // List of known Right-To-Left language codes.
+ sRtlLanguageCodes.add("ar"); // Arabic
+ sRtlLanguageCodes.add("fa"); // Persian
+ sRtlLanguageCodes.add("iw"); // Hebrew
+ sRtlLanguageCodes.add("ku"); // Kurdish
+ sRtlLanguageCodes.add("ps"); // Pashto
+ sRtlLanguageCodes.add("sd"); // Sindhi
+ sRtlLanguageCodes.add("ug"); // Uyghur
+ sRtlLanguageCodes.add("ur"); // Urdu
+ sRtlLanguageCodes.add("yi"); // Yiddish
+ }
+
+ public static boolean isRtlLanguage(@Nonnull final Locale locale) {
+ return sRtlLanguageCodes.contains(locale.getLanguage());
+ }
+}
diff --git a/common/src/com/android/inputmethod/latin/common/NativeSuggestOptions.java b/common/src/com/android/inputmethod/latin/common/NativeSuggestOptions.java
new file mode 100644
index 000000000..7ef741cc2
--- /dev/null
+++ b/common/src/com/android/inputmethod/latin/common/NativeSuggestOptions.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.latin.common;
+
+public class NativeSuggestOptions {
+ // Need to update suggest_options.h when you add, remove or reorder options.
+ private static final int IS_GESTURE = 0;
+ private static final int USE_FULL_EDIT_DISTANCE = 1;
+ private static final int BLOCK_OFFENSIVE_WORDS = 2;
+ private static final int SPACE_AWARE_GESTURE_ENABLED = 3;
+ private static final int WEIGHT_FOR_LOCALE_IN_THOUSANDS = 4;
+ private static final int OPTIONS_SIZE = 5;
+
+ private final int[] mOptions;
+
+ public NativeSuggestOptions(final int additionalFeaturesSettingsSize) {
+ mOptions = new int[OPTIONS_SIZE + additionalFeaturesSettingsSize];
+ }
+
+ public void setIsGesture(final boolean value) {
+ setBooleanOption(IS_GESTURE, value);
+ }
+
+ public void setUseFullEditDistance(final boolean value) {
+ setBooleanOption(USE_FULL_EDIT_DISTANCE, value);
+ }
+
+ public void setBlockOffensiveWords(final boolean value) {
+ setBooleanOption(BLOCK_OFFENSIVE_WORDS, value);
+ }
+
+ public void setSpaceAwareGestureEnabled(final boolean value) {
+ setBooleanOption(SPACE_AWARE_GESTURE_ENABLED, value);
+ }
+
+ public void setWeightForLocale(final float value) {
+ // We're passing this option as a fixed point value, in thousands. This is decoded in
+ // native code by SuggestOptions#weightForLocale().
+ setIntegerOption(WEIGHT_FOR_LOCALE_IN_THOUSANDS, (int) (value * 1000));
+ }
+
+ public void setAdditionalFeaturesOptions(final int[] additionalOptions) {
+ if (additionalOptions == null) {
+ return;
+ }
+ for (int i = 0; i < additionalOptions.length; i++) {
+ setIntegerOption(OPTIONS_SIZE + i, additionalOptions[i]);
+ }
+ }
+
+ public int[] getOptions() {
+ return mOptions;
+ }
+
+ private void setBooleanOption(final int key, final boolean value) {
+ mOptions[key] = value ? 1 : 0;
+ }
+
+ private void setIntegerOption(final int key, final int value) {
+ mOptions[key] = value;
+ }
+}
diff --git a/common/src/com/android/inputmethod/latin/common/ResizableIntArray.java b/common/src/com/android/inputmethod/latin/common/ResizableIntArray.java
new file mode 100644
index 000000000..340abb23e
--- /dev/null
+++ b/common/src/com/android/inputmethod/latin/common/ResizableIntArray.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.latin.common;
+
+import java.util.Arrays;
+
+import javax.annotation.Nonnull;
+
+// TODO: This class is not thread-safe.
+public final class ResizableIntArray {
+ @Nonnull
+ private int[] mArray;
+ private int mLength;
+
+ public ResizableIntArray(final int capacity) {
+ reset(capacity);
+ }
+
+ public int get(final int index) {
+ if (index < mLength) {
+ return mArray[index];
+ }
+ throw new ArrayIndexOutOfBoundsException("length=" + mLength + "; index=" + index);
+ }
+
+ public void addAt(final int index, final int val) {
+ if (index < mLength) {
+ mArray[index] = val;
+ } else {
+ mLength = index;
+ add(val);
+ }
+ }
+
+ public void add(final int val) {
+ final int currentLength = mLength;
+ ensureCapacity(currentLength + 1);
+ mArray[currentLength] = val;
+ mLength = currentLength + 1;
+ }
+
+ /**
+ * Calculate the new capacity of {@code mArray}.
+ * @param minimumCapacity the minimum capacity that the {@code mArray} should have.
+ * @return the new capacity that the {@code mArray} should have. Returns zero when there is no
+ * need to expand {@code mArray}.
+ */
+ private int calculateCapacity(final int minimumCapacity) {
+ final int currentCapcity = mArray.length;
+ if (currentCapcity < minimumCapacity) {
+ final int nextCapacity = currentCapcity * 2;
+ // The following is the same as return Math.max(minimumCapacity, nextCapacity);
+ return minimumCapacity > nextCapacity ? minimumCapacity : nextCapacity;
+ }
+ return 0;
+ }
+
+ private void ensureCapacity(final int minimumCapacity) {
+ final int newCapacity = calculateCapacity(minimumCapacity);
+ if (newCapacity > 0) {
+ // TODO: Implement primitive array pool.
+ mArray = Arrays.copyOf(mArray, newCapacity);
+ }
+ }
+
+ public int getLength() {
+ return mLength;
+ }
+
+ public void setLength(final int newLength) {
+ ensureCapacity(newLength);
+ mLength = newLength;
+ }
+
+ public void reset(final int capacity) {
+ // TODO: Implement primitive array pool.
+ mArray = new int[capacity];
+ mLength = 0;
+ }
+
+ @Nonnull
+ public int[] getPrimitiveArray() {
+ return mArray;
+ }
+
+ public void set(@Nonnull final ResizableIntArray ip) {
+ // TODO: Implement primitive array pool.
+ mArray = ip.mArray;
+ mLength = ip.mLength;
+ }
+
+ public void copy(@Nonnull final ResizableIntArray ip) {
+ final int newCapacity = calculateCapacity(ip.mLength);
+ if (newCapacity > 0) {
+ // TODO: Implement primitive array pool.
+ mArray = new int[newCapacity];
+ }
+ System.arraycopy(ip.mArray, 0, mArray, 0, ip.mLength);
+ mLength = ip.mLength;
+ }
+
+ public void append(@Nonnull final ResizableIntArray src, final int startPos, final int length) {
+ if (length == 0) {
+ return;
+ }
+ final int currentLength = mLength;
+ final int newLength = currentLength + length;
+ ensureCapacity(newLength);
+ System.arraycopy(src.mArray, startPos, mArray, currentLength, length);
+ mLength = newLength;
+ }
+
+ public void fill(final int value, final int startPos, final int length) {
+ if (startPos < 0 || length < 0) {
+ throw new IllegalArgumentException("startPos=" + startPos + "; length=" + length);
+ }
+ final int endPos = startPos + length;
+ ensureCapacity(endPos);
+ Arrays.fill(mArray, startPos, endPos, value);
+ if (mLength < endPos) {
+ mLength = endPos;
+ }
+ }
+
+ /**
+ * Shift to the left by elementCount, discarding elementCount pointers at the start.
+ * @param elementCount how many elements to shift.
+ */
+ public void shift(final int elementCount) {
+ System.arraycopy(mArray, elementCount, mArray, 0, mLength - elementCount);
+ mLength -= elementCount;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < mLength; i++) {
+ if (i != 0) {
+ sb.append(",");
+ }
+ sb.append(mArray[i]);
+ }
+ return "[" + sb + "]";
+ }
+}
diff --git a/common/src/com/android/inputmethod/latin/common/StringUtils.java b/common/src/com/android/inputmethod/latin/common/StringUtils.java
new file mode 100644
index 000000000..572f0cd9b
--- /dev/null
+++ b/common/src/com/android/inputmethod/latin/common/StringUtils.java
@@ -0,0 +1,704 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.latin.common;
+
+import com.android.inputmethod.annotations.UsedForTesting;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Locale;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+public final class StringUtils {
+ public static final int CAPITALIZE_NONE = 0; // No caps, or mixed case
+ public static final int CAPITALIZE_FIRST = 1; // First only
+ public static final int CAPITALIZE_ALL = 2; // All caps
+
+ @Nonnull
+ private static final String EMPTY_STRING = "";
+
+ private static final char CHAR_LINE_FEED = 0X000A;
+ private static final char CHAR_VERTICAL_TAB = 0X000B;
+ private static final char CHAR_FORM_FEED = 0X000C;
+ private static final char CHAR_CARRIAGE_RETURN = 0X000D;
+ private static final char CHAR_NEXT_LINE = 0X0085;
+ private static final char CHAR_LINE_SEPARATOR = 0X2028;
+ private static final char CHAR_PARAGRAPH_SEPARATOR = 0X2029;
+
+ private StringUtils() {
+ // This utility class is not publicly instantiable.
+ }
+
+ // Taken from android.text.TextUtils. We are extensively using this method in many places,
+ // some of which don't have the android libraries available.
+ /**
+ * Returns true if the string is null or 0-length.
+ * @param str the string to be examined
+ * @return true if str is null or zero length
+ */
+ public static boolean isEmpty(@Nullable final CharSequence str) {
+ return (str == null || str.length() == 0);
+ }
+
+ // Taken from android.text.TextUtils to cut the dependency to the Android framework.
+ /**
+ * Returns a string containing the tokens joined by delimiters.
+ * @param delimiter the delimiter
+ * @param tokens an array objects to be joined. Strings will be formed from
+ * the objects by calling object.toString().
+ */
+ @Nonnull
+ public static String join(@Nonnull final CharSequence delimiter,
+ @Nonnull final Iterable<?> tokens) {
+ final StringBuilder sb = new StringBuilder();
+ boolean firstTime = true;
+ for (final Object token: tokens) {
+ if (firstTime) {
+ firstTime = false;
+ } else {
+ sb.append(delimiter);
+ }
+ sb.append(token);
+ }
+ return sb.toString();
+ }
+
+ // Taken from android.text.TextUtils to cut the dependency to the Android framework.
+ /**
+ * Returns true if a and b are equal, including if they are both null.
+ * <p><i>Note: In platform versions 1.1 and earlier, this method only worked well if
+ * both the arguments were instances of String.</i></p>
+ * @param a first CharSequence to check
+ * @param b second CharSequence to check
+ * @return true if a and b are equal
+ */
+ public static boolean equals(@Nullable final CharSequence a, @Nullable final CharSequence b) {
+ if (a == b) {
+ return true;
+ }
+ final int length;
+ if (a != null && b != null && (length = a.length()) == b.length()) {
+ if (a instanceof String && b instanceof String) {
+ return a.equals(b);
+ }
+ for (int i = 0; i < length; i++) {
+ if (a.charAt(i) != b.charAt(i)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ public static int codePointCount(@Nullable final CharSequence text) {
+ if (isEmpty(text)) {
+ return 0;
+ }
+ return Character.codePointCount(text, 0, text.length());
+ }
+
+ @Nonnull
+ public static String newSingleCodePointString(final int codePoint) {
+ if (Character.charCount(codePoint) == 1) {
+ // Optimization: avoid creating a temporary array for characters that are
+ // represented by a single char value
+ return String.valueOf((char) codePoint);
+ }
+ // For surrogate pair
+ return new String(Character.toChars(codePoint));
+ }
+
+ public static boolean containsInArray(@Nonnull final String text,
+ @Nonnull final String[] array) {
+ for (final String element : array) {
+ if (text.equals(element)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Comma-Splittable Text is similar to Comma-Separated Values (CSV) but has much simpler syntax.
+ * Unlike CSV, Comma-Splittable Text has no escaping mechanism, so that the text can't contain
+ * a comma character in it.
+ */
+ @Nonnull
+ private static final String SEPARATOR_FOR_COMMA_SPLITTABLE_TEXT = ",";
+
+ public static boolean containsInCommaSplittableText(@Nonnull final String text,
+ @Nullable final String extraValues) {
+ if (isEmpty(extraValues)) {
+ return false;
+ }
+ return containsInArray(text, extraValues.split(SEPARATOR_FOR_COMMA_SPLITTABLE_TEXT));
+ }
+
+ @Nonnull
+ public static String removeFromCommaSplittableTextIfExists(@Nonnull final String text,
+ @Nullable final String extraValues) {
+ if (isEmpty(extraValues)) {
+ return EMPTY_STRING;
+ }
+ final String[] elements = extraValues.split(SEPARATOR_FOR_COMMA_SPLITTABLE_TEXT);
+ if (!containsInArray(text, elements)) {
+ return extraValues;
+ }
+ final ArrayList<String> result = new ArrayList<>(elements.length - 1);
+ for (final String element : elements) {
+ if (!text.equals(element)) {
+ result.add(element);
+ }
+ }
+ return join(SEPARATOR_FOR_COMMA_SPLITTABLE_TEXT, result);
+ }
+
+ /**
+ * Remove duplicates from an array of strings.
+ *
+ * This method will always keep the first occurrence of all strings at their position
+ * in the array, removing the subsequent ones.
+ */
+ public static void removeDupes(@Nonnull final ArrayList<String> suggestions) {
+ if (suggestions.size() < 2) {
+ return;
+ }
+ int i = 1;
+ // Don't cache suggestions.size(), since we may be removing items
+ while (i < suggestions.size()) {
+ final String cur = suggestions.get(i);
+ // Compare each suggestion with each previous suggestion
+ for (int j = 0; j < i; j++) {
+ final String previous = suggestions.get(j);
+ if (equals(cur, previous)) {
+ suggestions.remove(i);
+ i--;
+ break;
+ }
+ }
+ i++;
+ }
+ }
+
+ @Nonnull
+ public static String capitalizeFirstCodePoint(@Nonnull final String s,
+ @Nonnull final Locale locale) {
+ if (s.length() <= 1) {
+ return s.toUpperCase(getLocaleUsedForToTitleCase(locale));
+ }
+ // Please refer to the comment below in
+ // {@link #capitalizeFirstAndDowncaseRest(String,Locale)} as this has the same shortcomings
+ final int cutoff = s.offsetByCodePoints(0, 1);
+ return s.substring(0, cutoff).toUpperCase(getLocaleUsedForToTitleCase(locale))
+ + s.substring(cutoff);
+ }
+
+ @Nonnull
+ public static String capitalizeFirstAndDowncaseRest(@Nonnull final String s,
+ @Nonnull final Locale locale) {
+ if (s.length() <= 1) {
+ return s.toUpperCase(getLocaleUsedForToTitleCase(locale));
+ }
+ // TODO: fix the bugs below
+ // - It does not work for Serbian, because it fails to account for the "lj" character,
+ // which should be "Lj" in title case and "LJ" in upper case.
+ // - It does not work for Dutch, because it fails to account for the "ij" digraph when it's
+ // written as two separate code points. They are two different characters but both should
+ // be capitalized as "IJ" as if they were a single letter in most words (not all). If the
+ // unicode char for the ligature is used however, it works.
+ final int cutoff = s.offsetByCodePoints(0, 1);
+ return s.substring(0, cutoff).toUpperCase(getLocaleUsedForToTitleCase(locale))
+ + s.substring(cutoff).toLowerCase(locale);
+ }
+
+ @Nonnull
+ public static int[] toCodePointArray(@Nonnull final CharSequence charSequence) {
+ return toCodePointArray(charSequence, 0, charSequence.length());
+ }
+
+ @Nonnull
+ private static final int[] EMPTY_CODEPOINTS = {};
+
+ /**
+ * Converts a range of a string to an array of code points.
+ * @param charSequence the source string.
+ * @param startIndex the start index inside the string in java chars, inclusive.
+ * @param endIndex the end index inside the string in java chars, exclusive.
+ * @return a new array of code points. At most endIndex - startIndex, but possibly less.
+ */
+ @Nonnull
+ public static int[] toCodePointArray(@Nonnull final CharSequence charSequence,
+ final int startIndex, final int endIndex) {
+ final int length = charSequence.length();
+ if (length <= 0) {
+ return EMPTY_CODEPOINTS;
+ }
+ final int[] codePoints =
+ new int[Character.codePointCount(charSequence, startIndex, endIndex)];
+ copyCodePointsAndReturnCodePointCount(codePoints, charSequence, startIndex, endIndex,
+ false /* downCase */);
+ return codePoints;
+ }
+
+ /**
+ * Copies the codepoints in a CharSequence to an int array.
+ *
+ * This method assumes there is enough space in the array to store the code points. The size
+ * can be measured with Character#codePointCount(CharSequence, int, int) before passing to this
+ * method. If the int array is too small, an ArrayIndexOutOfBoundsException will be thrown.
+ * Also, this method makes no effort to be thread-safe. Do not modify the CharSequence while
+ * this method is running, or the behavior is undefined.
+ * This method can optionally downcase code points before copying them, but it pays no attention
+ * to locale while doing so.
+ *
+ * @param destination the int array.
+ * @param charSequence the CharSequence.
+ * @param startIndex the start index inside the string in java chars, inclusive.
+ * @param endIndex the end index inside the string in java chars, exclusive.
+ * @param downCase if this is true, code points will be downcased before being copied.
+ * @return the number of copied code points.
+ */
+ public static int copyCodePointsAndReturnCodePointCount(@Nonnull final int[] destination,
+ @Nonnull final CharSequence charSequence, final int startIndex, final int endIndex,
+ final boolean downCase) {
+ int destIndex = 0;
+ for (int index = startIndex; index < endIndex;
+ index = Character.offsetByCodePoints(charSequence, index, 1)) {
+ final int codePoint = Character.codePointAt(charSequence, index);
+ // TODO: stop using this, as it's not aware of the locale and does not always do
+ // the right thing.
+ destination[destIndex] = downCase ? Character.toLowerCase(codePoint) : codePoint;
+ destIndex++;
+ }
+ return destIndex;
+ }
+
+ @Nonnull
+ public static int[] toSortedCodePointArray(@Nonnull final String string) {
+ final int[] codePoints = toCodePointArray(string);
+ Arrays.sort(codePoints);
+ return codePoints;
+ }
+
+ /**
+ * Construct a String from a code point array
+ *
+ * @param codePoints a code point array that is null terminated when its logical length is
+ * shorter than the array length.
+ * @return a string constructed from the code point array.
+ */
+ @Nonnull
+ public static String getStringFromNullTerminatedCodePointArray(
+ @Nonnull final int[] codePoints) {
+ int stringLength = codePoints.length;
+ for (int i = 0; i < codePoints.length; i++) {
+ if (codePoints[i] == 0) {
+ stringLength = i;
+ break;
+ }
+ }
+ return new String(codePoints, 0 /* offset */, stringLength);
+ }
+
+ // This method assumes the text is not null. For the empty string, it returns CAPITALIZE_NONE.
+ public static int getCapitalizationType(@Nonnull final String text) {
+ // If the first char is not uppercase, then the word is either all lower case or
+ // camel case, and in either case we return CAPITALIZE_NONE.
+ final int len = text.length();
+ int index = 0;
+ for (; index < len; index = text.offsetByCodePoints(index, 1)) {
+ if (Character.isLetter(text.codePointAt(index))) {
+ break;
+ }
+ }
+ if (index == len) return CAPITALIZE_NONE;
+ if (!Character.isUpperCase(text.codePointAt(index))) {
+ return CAPITALIZE_NONE;
+ }
+ int capsCount = 1;
+ int letterCount = 1;
+ for (index = text.offsetByCodePoints(index, 1); index < len;
+ index = text.offsetByCodePoints(index, 1)) {
+ if (1 != capsCount && letterCount != capsCount) break;
+ final int codePoint = text.codePointAt(index);
+ if (Character.isUpperCase(codePoint)) {
+ ++capsCount;
+ ++letterCount;
+ } else if (Character.isLetter(codePoint)) {
+ // We need to discount non-letters since they may not be upper-case, but may
+ // still be part of a word (e.g. single quote or dash, as in "IT'S" or "FULL-TIME")
+ ++letterCount;
+ }
+ }
+ // We know the first char is upper case. So we want to test if either every letter other
+ // than the first is lower case, or if they are all upper case. If the string is exactly
+ // one char long, then we will arrive here with letterCount 1, and this is correct, too.
+ if (1 == capsCount) return CAPITALIZE_FIRST;
+ return (letterCount == capsCount ? CAPITALIZE_ALL : CAPITALIZE_NONE);
+ }
+
+ public static boolean isIdenticalAfterUpcase(@Nonnull final String text) {
+ final int length = text.length();
+ int i = 0;
+ while (i < length) {
+ final int codePoint = text.codePointAt(i);
+ if (Character.isLetter(codePoint) && !Character.isUpperCase(codePoint)) {
+ return false;
+ }
+ i += Character.charCount(codePoint);
+ }
+ return true;
+ }
+
+ public static boolean isIdenticalAfterDowncase(@Nonnull final String text) {
+ final int length = text.length();
+ int i = 0;
+ while (i < length) {
+ final int codePoint = text.codePointAt(i);
+ if (Character.isLetter(codePoint) && !Character.isLowerCase(codePoint)) {
+ return false;
+ }
+ i += Character.charCount(codePoint);
+ }
+ return true;
+ }
+
+ public static boolean isIdenticalAfterCapitalizeEachWord(@Nonnull final String text,
+ @Nonnull final int[] sortedSeparators) {
+ boolean needsCapsNext = true;
+ final int len = text.length();
+ for (int i = 0; i < len; i = text.offsetByCodePoints(i, 1)) {
+ final int codePoint = text.codePointAt(i);
+ if (Character.isLetter(codePoint)) {
+ if ((needsCapsNext && !Character.isUpperCase(codePoint))
+ || (!needsCapsNext && !Character.isLowerCase(codePoint))) {
+ return false;
+ }
+ }
+ // We need a capital letter next if this is a separator.
+ needsCapsNext = (Arrays.binarySearch(sortedSeparators, codePoint) >= 0);
+ }
+ return true;
+ }
+
+ // TODO: like capitalizeFirst*, this does not work perfectly for Dutch because of the IJ digraph
+ // which should be capitalized together in *some* cases.
+ @Nonnull
+ public static String capitalizeEachWord(@Nonnull final String text,
+ @Nonnull final int[] sortedSeparators, @Nonnull final Locale locale) {
+ final StringBuilder builder = new StringBuilder();
+ boolean needsCapsNext = true;
+ final int len = text.length();
+ for (int i = 0; i < len; i = text.offsetByCodePoints(i, 1)) {
+ final String nextChar = text.substring(i, text.offsetByCodePoints(i, 1));
+ if (needsCapsNext) {
+ builder.append(nextChar.toUpperCase(locale));
+ } else {
+ builder.append(nextChar.toLowerCase(locale));
+ }
+ // We need a capital letter next if this is a separator.
+ needsCapsNext = (Arrays.binarySearch(sortedSeparators, nextChar.codePointAt(0)) >= 0);
+ }
+ return builder.toString();
+ }
+
+ /**
+ * Approximates whether the text before the cursor looks like a URL.
+ *
+ * This is not foolproof, but it should work well in the practice.
+ * Essentially it walks backward from the cursor until it finds something that's not a letter,
+ * digit, or common URL symbol like underscore. If it hasn't found a period yet, then it
+ * does not look like a URL.
+ * If the text:
+ * - starts with www and contains a period
+ * - starts with a slash preceded by either a slash, whitespace, or start-of-string
+ * Then it looks like a URL and we return true. Otherwise, we return false.
+ *
+ * Note: this method is called quite often, and should be fast.
+ *
+ * TODO: This will return that "abc./def" and ".abc/def" look like URLs to keep down the
+ * code complexity, but ideally it should not. It's acceptable for now.
+ */
+ public static boolean lastPartLooksLikeURL(@Nonnull final CharSequence text) {
+ int i = text.length();
+ if (0 == i) {
+ return false;
+ }
+ int wCount = 0;
+ int slashCount = 0;
+ boolean hasSlash = false;
+ boolean hasPeriod = false;
+ int codePoint = 0;
+ while (i > 0) {
+ codePoint = Character.codePointBefore(text, i);
+ if (codePoint < Constants.CODE_PERIOD || codePoint > 'z') {
+ // Handwavy heuristic to see if that's a URL character. Anything between period
+ // and z. This includes all lower- and upper-case ascii letters, period,
+ // underscore, arrobase, question mark, equal sign. It excludes spaces, exclamation
+ // marks, double quotes...
+ // Anything that's not a URL-like character causes us to break from here and
+ // evaluate normally.
+ break;
+ }
+ if (Constants.CODE_PERIOD == codePoint) {
+ hasPeriod = true;
+ }
+ if (Constants.CODE_SLASH == codePoint) {
+ hasSlash = true;
+ if (2 == ++slashCount) {
+ return true;
+ }
+ } else {
+ slashCount = 0;
+ }
+ if ('w' == codePoint) {
+ ++wCount;
+ } else {
+ wCount = 0;
+ }
+ i = Character.offsetByCodePoints(text, i, -1);
+ }
+ // End of the text run.
+ // If it starts with www and includes a period, then it looks like a URL.
+ if (wCount >= 3 && hasPeriod) {
+ return true;
+ }
+ // If it starts with a slash, and the code point before is whitespace, it looks like an URL.
+ if (1 == slashCount && (0 == i || Character.isWhitespace(codePoint))) {
+ return true;
+ }
+ // If it has both a period and a slash, it looks like an URL.
+ if (hasPeriod && hasSlash) {
+ return true;
+ }
+ // Otherwise, it doesn't look like an URL.
+ return false;
+ }
+
+ /**
+ * Examines the string and returns whether we're inside a double quote.
+ *
+ * This is used to decide whether we should put an automatic space before or after a double
+ * quote character. If we're inside a quotation, then we want to close it, so we want a space
+ * after and not before. Otherwise, we want to open the quotation, so we want a space before
+ * and not after. Exception: after a digit, we never want a space because the "inch" or
+ * "minutes" use cases is dominant after digits.
+ * In the practice, we determine whether we are in a quotation or not by finding the previous
+ * double quote character, and looking at whether it's followed by whitespace. If so, that
+ * was a closing quotation mark, so we're not inside a double quote. If it's not followed
+ * by whitespace, then it was an opening quotation mark, and we're inside a quotation.
+ *
+ * @param text the text to examine.
+ * @return whether we're inside a double quote.
+ */
+ public static boolean isInsideDoubleQuoteOrAfterDigit(@Nonnull final CharSequence text) {
+ int i = text.length();
+ if (0 == i) {
+ return false;
+ }
+ int codePoint = Character.codePointBefore(text, i);
+ if (Character.isDigit(codePoint)) {
+ return true;
+ }
+ int prevCodePoint = 0;
+ while (i > 0) {
+ codePoint = Character.codePointBefore(text, i);
+ if (Constants.CODE_DOUBLE_QUOTE == codePoint) {
+ // If we see a double quote followed by whitespace, then that
+ // was a closing quote.
+ if (Character.isWhitespace(prevCodePoint)) {
+ return false;
+ }
+ }
+ if (Character.isWhitespace(codePoint) && Constants.CODE_DOUBLE_QUOTE == prevCodePoint) {
+ // If we see a double quote preceded by whitespace, then that
+ // was an opening quote. No need to continue seeking.
+ return true;
+ }
+ i -= Character.charCount(codePoint);
+ prevCodePoint = codePoint;
+ }
+ // We reached the start of text. If the first char is a double quote, then we're inside
+ // a double quote. Otherwise we're not.
+ return Constants.CODE_DOUBLE_QUOTE == codePoint;
+ }
+
+ public static boolean isEmptyStringOrWhiteSpaces(@Nonnull final String s) {
+ final int N = codePointCount(s);
+ for (int i = 0; i < N; ++i) {
+ if (!Character.isWhitespace(s.codePointAt(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @UsedForTesting
+ @Nonnull
+ public static String byteArrayToHexString(@Nullable final byte[] bytes) {
+ if (bytes == null || bytes.length == 0) {
+ return EMPTY_STRING;
+ }
+ final StringBuilder sb = new StringBuilder();
+ for (final byte b : bytes) {
+ sb.append(String.format("%02x", b & 0xff));
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Convert hex string to byte array. The string length must be an even number.
+ */
+ @UsedForTesting
+ @Nullable
+ public static byte[] hexStringToByteArray(@Nullable final String hexString) {
+ if (isEmpty(hexString)) {
+ return null;
+ }
+ final int N = hexString.length();
+ if (N % 2 != 0) {
+ throw new NumberFormatException("Input hex string length must be an even number."
+ + " Length = " + N);
+ }
+ final byte[] bytes = new byte[N / 2];
+ for (int i = 0; i < N; i += 2) {
+ bytes[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4)
+ + Character.digit(hexString.charAt(i + 1), 16));
+ }
+ return bytes;
+ }
+
+ private static final String LANGUAGE_GREEK = "el";
+
+ @Nonnull
+ private static Locale getLocaleUsedForToTitleCase(@Nonnull final Locale locale) {
+ // In Greek locale {@link String#toUpperCase(Locale)} eliminates accents from its result.
+ // In order to get accented upper case letter, {@link Locale#ROOT} should be used.
+ if (LANGUAGE_GREEK.equals(locale.getLanguage())) {
+ return Locale.ROOT;
+ }
+ return locale;
+ }
+
+ @Nullable
+ public static String toTitleCaseOfKeyLabel(@Nullable final String label,
+ @Nonnull final Locale locale) {
+ if (label == null) {
+ return label;
+ }
+ return label.toUpperCase(getLocaleUsedForToTitleCase(locale));
+ }
+
+ public static int toTitleCaseOfKeyCode(final int code, @Nonnull final Locale locale) {
+ if (!Constants.isLetterCode(code)) {
+ return code;
+ }
+ final String label = newSingleCodePointString(code);
+ final String titleCaseLabel = toTitleCaseOfKeyLabel(label, locale);
+ return codePointCount(titleCaseLabel) == 1
+ ? titleCaseLabel.codePointAt(0) : Constants.CODE_UNSPECIFIED;
+ }
+
+ public static int getTrailingSingleQuotesCount(@Nonnull final CharSequence charSequence) {
+ final int lastIndex = charSequence.length() - 1;
+ int i = lastIndex;
+ while (i >= 0 && charSequence.charAt(i) == Constants.CODE_SINGLE_QUOTE) {
+ --i;
+ }
+ return lastIndex - i;
+ }
+
+ @UsedForTesting
+ public static class Stringizer<E> {
+ @Nonnull
+ private static final String[] EMPTY_STRING_ARRAY = new String[0];
+
+ @UsedForTesting
+ @Nonnull
+ public String stringize(@Nullable final E element) {
+ if (element == null) {
+ return "null";
+ }
+ return element.toString();
+ }
+
+ @UsedForTesting
+ @Nonnull
+ public final String join(@Nullable final E[] array) {
+ return joinStringArray(toStringArray(array), null /* delimiter */);
+ }
+
+ @UsedForTesting
+ public final String join(@Nullable final E[] array, @Nullable final String delimiter) {
+ return joinStringArray(toStringArray(array), delimiter);
+ }
+
+ @Nonnull
+ protected String[] toStringArray(@Nullable final E[] array) {
+ if (array == null) {
+ return EMPTY_STRING_ARRAY;
+ }
+ final String[] stringArray = new String[array.length];
+ for (int index = 0; index < array.length; index++) {
+ stringArray[index] = stringize(array[index]);
+ }
+ return stringArray;
+ }
+
+ @Nonnull
+ protected String joinStringArray(@Nonnull final String[] stringArray,
+ @Nullable final String delimiter) {
+ if (delimiter == null) {
+ return Arrays.toString(stringArray);
+ }
+ final StringBuilder sb = new StringBuilder();
+ for (int index = 0; index < stringArray.length; index++) {
+ sb.append(index == 0 ? "[" : delimiter);
+ sb.append(stringArray[index]);
+ }
+ return sb + "]";
+ }
+ }
+
+ /**
+ * Returns whether the last composed word contains line-breaking character (e.g. CR or LF).
+ * @param text the text to be examined.
+ * @return {@code true} if the last composed word contains line-breaking separator.
+ */
+ public static boolean hasLineBreakCharacter(@Nullable final String text) {
+ if (isEmpty(text)) {
+ return false;
+ }
+ for (int i = text.length() - 1; i >= 0; --i) {
+ final char c = text.charAt(i);
+ switch (c) {
+ case CHAR_LINE_FEED:
+ case CHAR_VERTICAL_TAB:
+ case CHAR_FORM_FEED:
+ case CHAR_CARRIAGE_RETURN:
+ case CHAR_NEXT_LINE:
+ case CHAR_LINE_SEPARATOR:
+ case CHAR_PARAGRAPH_SEPARATOR:
+ return true;
+ }
+ }
+ return false;
+ }
+}