aboutsummaryrefslogtreecommitdiffstats
path: root/java/src/com/android/inputmethod/latin/utils
diff options
context:
space:
mode:
Diffstat (limited to 'java/src/com/android/inputmethod/latin/utils')
-rw-r--r--java/src/com/android/inputmethod/latin/utils/AdditionalSubtypeUtils.java140
-rw-r--r--java/src/com/android/inputmethod/latin/utils/ApplicationUtils.java65
-rw-r--r--java/src/com/android/inputmethod/latin/utils/AsyncResultHolder.java71
-rw-r--r--java/src/com/android/inputmethod/latin/utils/AutoCorrectionUtils.java139
-rw-r--r--java/src/com/android/inputmethod/latin/utils/Base64Reader.java117
-rw-r--r--java/src/com/android/inputmethod/latin/utils/BoundedTreeSet.java49
-rw-r--r--java/src/com/android/inputmethod/latin/utils/ByteArrayDictBuffer.java81
-rw-r--r--java/src/com/android/inputmethod/latin/utils/CapsModeUtils.java274
-rw-r--r--java/src/com/android/inputmethod/latin/utils/CollectionUtils.java105
-rw-r--r--java/src/com/android/inputmethod/latin/utils/CompletionInfoUtils.java43
-rw-r--r--java/src/com/android/inputmethod/latin/utils/CoordinateUtils.java49
-rw-r--r--java/src/com/android/inputmethod/latin/utils/CsvUtils.java318
-rw-r--r--java/src/com/android/inputmethod/latin/utils/DebugLogUtils.java115
-rw-r--r--java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java358
-rw-r--r--java/src/com/android/inputmethod/latin/utils/FeedbackUtils.java37
-rw-r--r--java/src/com/android/inputmethod/latin/utils/FileTransforms.java38
-rw-r--r--java/src/com/android/inputmethod/latin/utils/InputTypeUtils.java117
-rw-r--r--java/src/com/android/inputmethod/latin/utils/IntentUtils.java45
-rw-r--r--java/src/com/android/inputmethod/latin/utils/JniUtils.java41
-rw-r--r--java/src/com/android/inputmethod/latin/utils/LatinImeLoggerUtils.java77
-rw-r--r--java/src/com/android/inputmethod/latin/utils/LocaleUtils.java225
-rw-r--r--java/src/com/android/inputmethod/latin/utils/MetadataFileUriGetter.java38
-rw-r--r--java/src/com/android/inputmethod/latin/utils/PrioritizedSerialExecutor.java147
-rw-r--r--java/src/com/android/inputmethod/latin/utils/RecapitalizeStatus.java190
-rw-r--r--java/src/com/android/inputmethod/latin/utils/ResizableIntArray.java155
-rw-r--r--java/src/com/android/inputmethod/latin/utils/ResourceUtils.java323
-rw-r--r--java/src/com/android/inputmethod/latin/utils/RunInLocale.java55
-rw-r--r--java/src/com/android/inputmethod/latin/utils/SpannableStringUtils.java110
-rw-r--r--java/src/com/android/inputmethod/latin/utils/StaticInnerHandlerWrapper.java42
-rw-r--r--java/src/com/android/inputmethod/latin/utils/StringUtils.java465
-rw-r--r--java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java333
-rw-r--r--java/src/com/android/inputmethod/latin/utils/TargetPackageInfoGetterTask.java70
-rw-r--r--java/src/com/android/inputmethod/latin/utils/TextRange.java120
-rw-r--r--java/src/com/android/inputmethod/latin/utils/TypefaceUtils.java94
-rw-r--r--java/src/com/android/inputmethod/latin/utils/UsabilityStudyLogUtils.java293
-rw-r--r--java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java173
-rw-r--r--java/src/com/android/inputmethod/latin/utils/UserHistoryForgettingCurveUtils.java233
-rw-r--r--java/src/com/android/inputmethod/latin/utils/UserLogRingCharBuffer.java137
-rw-r--r--java/src/com/android/inputmethod/latin/utils/ViewLayoutUtils.java54
-rw-r--r--java/src/com/android/inputmethod/latin/utils/XmlParseUtils.java83
40 files changed, 5619 insertions, 0 deletions
diff --git a/java/src/com/android/inputmethod/latin/utils/AdditionalSubtypeUtils.java b/java/src/com/android/inputmethod/latin/utils/AdditionalSubtypeUtils.java
new file mode 100644
index 000000000..44b201642
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/AdditionalSubtypeUtils.java
@@ -0,0 +1,140 @@
+/*
+ * 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.utils;
+
+import static com.android.inputmethod.latin.Constants.Subtype.KEYBOARD_MODE;
+import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.IS_ADDITIONAL_SUBTYPE;
+import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET;
+import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME;
+
+import android.os.Build;
+import android.text.TextUtils;
+import android.view.inputmethod.InputMethodSubtype;
+
+import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.R;
+
+import java.util.ArrayList;
+
+public final class AdditionalSubtypeUtils {
+ private static final InputMethodSubtype[] EMPTY_SUBTYPE_ARRAY = new InputMethodSubtype[0];
+
+ private AdditionalSubtypeUtils() {
+ // This utility class is not publicly instantiable.
+ }
+
+ public static boolean isAdditionalSubtype(final InputMethodSubtype subtype) {
+ return subtype.containsExtraValueKey(IS_ADDITIONAL_SUBTYPE);
+ }
+
+ private static final String LOCALE_AND_LAYOUT_SEPARATOR = ":";
+ private static final String PREF_SUBTYPE_SEPARATOR = ";";
+
+ public static InputMethodSubtype createAdditionalSubtype(final String localeString,
+ final String keyboardLayoutSetName, final String extraValue) {
+ final String layoutExtraValue = KEYBOARD_LAYOUT_SET + "=" + keyboardLayoutSetName;
+ final String layoutDisplayNameExtraValue;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN
+ && SubtypeLocaleUtils.isExceptionalLocale(localeString)) {
+ final String layoutDisplayName = SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(
+ keyboardLayoutSetName);
+ layoutDisplayNameExtraValue = StringUtils.appendToCommaSplittableTextIfNotExists(
+ UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME + "=" + layoutDisplayName, extraValue);
+ } else {
+ layoutDisplayNameExtraValue = extraValue;
+ }
+ final String additionalSubtypeExtraValue =
+ StringUtils.appendToCommaSplittableTextIfNotExists(
+ IS_ADDITIONAL_SUBTYPE, layoutDisplayNameExtraValue);
+ final int nameId = SubtypeLocaleUtils.getSubtypeNameId(localeString, keyboardLayoutSetName);
+ return new InputMethodSubtype(nameId, R.drawable.ic_ime_switcher_dark,
+ localeString, KEYBOARD_MODE, layoutExtraValue + "," + additionalSubtypeExtraValue
+ + "," + Constants.Subtype.ExtraValue.ASCII_CAPABLE
+ + "," + Constants.Subtype.ExtraValue.EMOJI_CAPABLE, false, false);
+ }
+
+ public static String getPrefSubtype(final InputMethodSubtype subtype) {
+ final String localeString = subtype.getLocale();
+ final String keyboardLayoutSetName = SubtypeLocaleUtils.getKeyboardLayoutSetName(subtype);
+ final String layoutExtraValue = KEYBOARD_LAYOUT_SET + "=" + keyboardLayoutSetName;
+ final String extraValue = StringUtils.removeFromCommaSplittableTextIfExists(
+ layoutExtraValue, StringUtils.removeFromCommaSplittableTextIfExists(
+ IS_ADDITIONAL_SUBTYPE, subtype.getExtraValue()));
+ final String basePrefSubtype = localeString + LOCALE_AND_LAYOUT_SEPARATOR
+ + keyboardLayoutSetName;
+ return extraValue.isEmpty() ? basePrefSubtype
+ : basePrefSubtype + LOCALE_AND_LAYOUT_SEPARATOR + extraValue;
+ }
+
+ public static InputMethodSubtype createAdditionalSubtype(final String prefSubtype) {
+ final String elems[] = prefSubtype.split(LOCALE_AND_LAYOUT_SEPARATOR);
+ if (elems.length < 2 || elems.length > 3) {
+ throw new RuntimeException("Unknown additional subtype specified: " + prefSubtype);
+ }
+ final String localeString = elems[0];
+ final String keyboardLayoutSetName = elems[1];
+ final String extraValue = (elems.length == 3) ? elems[2] : null;
+ return createAdditionalSubtype(localeString, keyboardLayoutSetName, extraValue);
+ }
+
+ public static InputMethodSubtype[] createAdditionalSubtypesArray(final String prefSubtypes) {
+ if (TextUtils.isEmpty(prefSubtypes)) {
+ return EMPTY_SUBTYPE_ARRAY;
+ }
+ final String[] prefSubtypeArray = prefSubtypes.split(PREF_SUBTYPE_SEPARATOR);
+ final ArrayList<InputMethodSubtype> subtypesList =
+ CollectionUtils.newArrayList(prefSubtypeArray.length);
+ for (final String prefSubtype : prefSubtypeArray) {
+ final InputMethodSubtype subtype = createAdditionalSubtype(prefSubtype);
+ if (subtype.getNameResId() == SubtypeLocaleUtils.UNKNOWN_KEYBOARD_LAYOUT) {
+ // Skip unknown keyboard layout subtype. This may happen when predefined keyboard
+ // layout has been removed.
+ continue;
+ }
+ subtypesList.add(subtype);
+ }
+ return subtypesList.toArray(new InputMethodSubtype[subtypesList.size()]);
+ }
+
+ public static String createPrefSubtypes(final InputMethodSubtype[] subtypes) {
+ if (subtypes == null || subtypes.length == 0) {
+ return "";
+ }
+ final StringBuilder sb = new StringBuilder();
+ for (final InputMethodSubtype subtype : subtypes) {
+ if (sb.length() > 0) {
+ sb.append(PREF_SUBTYPE_SEPARATOR);
+ }
+ sb.append(getPrefSubtype(subtype));
+ }
+ return sb.toString();
+ }
+
+ public static String createPrefSubtypes(final String[] prefSubtypes) {
+ if (prefSubtypes == null || prefSubtypes.length == 0) {
+ return "";
+ }
+ final StringBuilder sb = new StringBuilder();
+ for (final String prefSubtype : prefSubtypes) {
+ if (sb.length() > 0) {
+ sb.append(PREF_SUBTYPE_SEPARATOR);
+ }
+ sb.append(prefSubtype);
+ }
+ return sb.toString();
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/ApplicationUtils.java b/java/src/com/android/inputmethod/latin/utils/ApplicationUtils.java
new file mode 100644
index 000000000..08a2a8c5a
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/ApplicationUtils.java
@@ -0,0 +1,65 @@
+/*
+ * 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.utils;
+
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.util.Log;
+
+public final class ApplicationUtils {
+ private static final String TAG = ApplicationUtils.class.getSimpleName();
+
+ private ApplicationUtils() {
+ // This utility class is not publicly instantiable.
+ }
+
+ public static int getAcitivityTitleResId(final Context context,
+ final Class<? extends Activity> cls) {
+ final ComponentName cn = new ComponentName(context, cls);
+ try {
+ final ActivityInfo ai = context.getPackageManager().getActivityInfo(cn, 0);
+ if (ai != null) {
+ return ai.labelRes;
+ }
+ } catch (final NameNotFoundException e) {
+ Log.e(TAG, "Failed to get settings activity title res id.", e);
+ }
+ return 0;
+ }
+
+ /**
+ * A utility method to get the application's PackageInfo.versionName
+ * @return the application's PackageInfo.versionName
+ */
+ public static String getVersionName(final Context context) {
+ try {
+ if (context == null) {
+ return "";
+ }
+ final String packageName = context.getPackageName();
+ final PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
+ return info.versionName;
+ } catch (final NameNotFoundException e) {
+ Log.e(TAG, "Could not find version info.", e);
+ }
+ return "";
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/AsyncResultHolder.java b/java/src/com/android/inputmethod/latin/utils/AsyncResultHolder.java
new file mode 100644
index 000000000..c2e97a36f
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/AsyncResultHolder.java
@@ -0,0 +1,71 @@
+/*
+ * 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.utils;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * This class is a holder of a result of asynchronous computation.
+ *
+ * @param <E> the type of the result.
+ */
+public class AsyncResultHolder<E> {
+
+ private final Object mLock = new Object();
+
+ private E mResult;
+ private final CountDownLatch mLatch;
+
+ public AsyncResultHolder() {
+ mLatch = new CountDownLatch(1);
+ }
+
+ /**
+ * Sets the result value to this holder.
+ *
+ * @param result the value which is set.
+ */
+ public void set(final E result) {
+ synchronized(mLock) {
+ if (mLatch.getCount() > 0) {
+ mResult = result;
+ mLatch.countDown();
+ }
+ }
+ }
+
+ /**
+ * Gets the result value held in this holder.
+ * Causes the current thread to wait unless the value is set or the specified time is elapsed.
+ *
+ * @param defaultValue the default value.
+ * @param timeOut the time to wait.
+ * @return if the result is set until the time limit then the result, otherwise defaultValue.
+ */
+ public E get(final E defaultValue, final long timeOut) {
+ try {
+ if(mLatch.await(timeOut, TimeUnit.MILLISECONDS)) {
+ return mResult;
+ } else {
+ return defaultValue;
+ }
+ } catch (InterruptedException e) {
+ return defaultValue;
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/AutoCorrectionUtils.java b/java/src/com/android/inputmethod/latin/utils/AutoCorrectionUtils.java
new file mode 100644
index 000000000..066c5fd32
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/AutoCorrectionUtils.java
@@ -0,0 +1,139 @@
+/*
+ * 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.utils;
+
+import com.android.inputmethod.latin.BinaryDictionary;
+import com.android.inputmethod.latin.Dictionary;
+import com.android.inputmethod.latin.LatinImeLogger;
+import com.android.inputmethod.latin.Suggest;
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.concurrent.ConcurrentHashMap;
+
+public final class AutoCorrectionUtils {
+ private static final boolean DBG = LatinImeLogger.sDBG;
+ private static final String TAG = AutoCorrectionUtils.class.getSimpleName();
+ private static final int MINIMUM_SAFETY_NET_CHAR_LENGTH = 4;
+
+ private AutoCorrectionUtils() {
+ // Purely static class: can't instantiate.
+ }
+
+ public static boolean isValidWord(final Suggest suggest, final String word,
+ final boolean ignoreCase) {
+ if (TextUtils.isEmpty(word)) {
+ return false;
+ }
+ final ConcurrentHashMap<String, Dictionary> dictionaries = suggest.getUnigramDictionaries();
+ final String lowerCasedWord = word.toLowerCase(suggest.mLocale);
+ for (final String key : dictionaries.keySet()) {
+ final Dictionary dictionary = dictionaries.get(key);
+ // It's unclear how realistically 'dictionary' can be null, but the monkey is somehow
+ // managing to get null in here. Presumably the language is changing to a language with
+ // no main dictionary and the monkey manages to type a whole word before the thread
+ // that reads the dictionary is started or something?
+ // Ideally the passed map would come out of a {@link java.util.concurrent.Future} and
+ // would be immutable once it's finished initializing, but concretely a null test is
+ // probably good enough for the time being.
+ if (null == dictionary) continue;
+ if (dictionary.isValidWord(word)
+ || (ignoreCase && dictionary.isValidWord(lowerCasedWord))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static int getMaxFrequency(final ConcurrentHashMap<String, Dictionary> dictionaries,
+ final String word) {
+ if (TextUtils.isEmpty(word)) {
+ return Dictionary.NOT_A_PROBABILITY;
+ }
+ int maxFreq = -1;
+ for (final String key : dictionaries.keySet()) {
+ final Dictionary dictionary = dictionaries.get(key);
+ if (null == dictionary) continue;
+ final int tempFreq = dictionary.getFrequency(word);
+ if (tempFreq >= maxFreq) {
+ maxFreq = tempFreq;
+ }
+ }
+ return maxFreq;
+ }
+
+ public static boolean suggestionExceedsAutoCorrectionThreshold(
+ final SuggestedWordInfo suggestion, final String consideredWord,
+ final float autoCorrectionThreshold) {
+ if (null != suggestion) {
+ // Shortlist a whitelisted word
+ if (suggestion.mKind == SuggestedWordInfo.KIND_WHITELIST) return true;
+ final int autoCorrectionSuggestionScore = suggestion.mScore;
+ // TODO: when the normalized score of the first suggestion is nearly equals to
+ // the normalized score of the second suggestion, behave less aggressive.
+ final float normalizedScore = BinaryDictionary.calcNormalizedScore(
+ consideredWord, suggestion.mWord, autoCorrectionSuggestionScore);
+ if (DBG) {
+ Log.d(TAG, "Normalized " + consideredWord + "," + suggestion + ","
+ + autoCorrectionSuggestionScore + ", " + normalizedScore
+ + "(" + autoCorrectionThreshold + ")");
+ }
+ if (normalizedScore >= autoCorrectionThreshold) {
+ if (DBG) {
+ Log.d(TAG, "Auto corrected by S-threshold.");
+ }
+ return !shouldBlockAutoCorrectionBySafetyNet(consideredWord, suggestion.mWord);
+ }
+ }
+ return false;
+ }
+
+ // TODO: Resolve the inconsistencies between the native auto correction algorithms and
+ // this safety net
+ public static boolean shouldBlockAutoCorrectionBySafetyNet(final String typedWord,
+ final String suggestion) {
+ // Safety net for auto correction.
+ // Actually if we hit this safety net, it's a bug.
+ // If user selected aggressive auto correction mode, there is no need to use the safety
+ // net.
+ // If the length of typed word is less than MINIMUM_SAFETY_NET_CHAR_LENGTH,
+ // we should not use net because relatively edit distance can be big.
+ final int typedWordLength = typedWord.length();
+ if (typedWordLength < MINIMUM_SAFETY_NET_CHAR_LENGTH) {
+ return false;
+ }
+ final int maxEditDistanceOfNativeDictionary =
+ (typedWordLength < 5 ? 2 : typedWordLength / 2) + 1;
+ final int distance = BinaryDictionary.editDistance(typedWord, suggestion);
+ if (DBG) {
+ Log.d(TAG, "Autocorrected edit distance = " + distance
+ + ", " + maxEditDistanceOfNativeDictionary);
+ }
+ if (distance > maxEditDistanceOfNativeDictionary) {
+ if (DBG) {
+ Log.e(TAG, "Safety net: before = " + typedWord + ", after = " + suggestion);
+ Log.e(TAG, "(Error) The edit distance of this correction exceeds limit. "
+ + "Turning off auto-correction.");
+ }
+ return true;
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/Base64Reader.java b/java/src/com/android/inputmethod/latin/utils/Base64Reader.java
new file mode 100644
index 000000000..3eca6e744
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/Base64Reader.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.utils;
+
+import com.android.inputmethod.annotations.UsedForTesting;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.LineNumberReader;
+
+@UsedForTesting
+public class Base64Reader {
+ private final LineNumberReader mReader;
+
+ private String mLine;
+ private int mCharPos;
+ private int mByteCount;
+
+ @UsedForTesting
+ public Base64Reader(final LineNumberReader reader) {
+ mReader = reader;
+ reset();
+ }
+
+ @UsedForTesting
+ public void reset() {
+ mLine = null;
+ mCharPos = 0;
+ mByteCount = 0;
+ }
+
+ @UsedForTesting
+ public int getLineNumber() {
+ return mReader.getLineNumber();
+ }
+
+ @UsedForTesting
+ public int getByteCount() {
+ return mByteCount;
+ }
+
+ private void fillBuffer() throws IOException {
+ if (mLine == null || mCharPos >= mLine.length()) {
+ mLine = mReader.readLine();
+ mCharPos = 0;
+ }
+ if (mLine == null) {
+ throw new EOFException();
+ }
+ }
+
+ private int peekUint8() throws IOException {
+ fillBuffer();
+ final char c = mLine.charAt(mCharPos);
+ if (c >= 'A' && c <= 'Z')
+ return c - 'A' + 0;
+ if (c >= 'a' && c <= 'z')
+ return c - 'a' + 26;
+ if (c >= '0' && c <= '9')
+ return c - '0' + 52;
+ if (c == '+')
+ return 62;
+ if (c == '/')
+ return 63;
+ if (c == '=')
+ return 0;
+ throw new RuntimeException("Unknown character '" + c + "' in base64 at line "
+ + mReader.getLineNumber());
+ }
+
+ private int getUint8() throws IOException {
+ final int value = peekUint8();
+ mCharPos++;
+ return value;
+ }
+
+ @UsedForTesting
+ public int readUint8() throws IOException {
+ final int value1, value2;
+ switch (mByteCount % 3) {
+ case 0:
+ value1 = getUint8() << 2;
+ value2 = value1 | (peekUint8() >> 4);
+ break;
+ case 1:
+ value1 = (getUint8() & 0x0f) << 4;
+ value2 = value1 | (peekUint8() >> 2);
+ break;
+ default:
+ value1 = (getUint8() & 0x03) << 6;
+ value2 = value1 | getUint8();
+ break;
+ }
+ mByteCount++;
+ return value2;
+ }
+
+ @UsedForTesting
+ public short readInt16() throws IOException {
+ final int data = readUint8() << 8;
+ return (short)(data | readUint8());
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/BoundedTreeSet.java b/java/src/com/android/inputmethod/latin/utils/BoundedTreeSet.java
new file mode 100644
index 000000000..ae1fd3f79
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/BoundedTreeSet.java
@@ -0,0 +1,49 @@
+/*
+ * 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.utils;
+
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.TreeSet;
+
+/**
+ * A TreeSet that is bounded in size and throws everything that's smaller than its limit
+ */
+public final class BoundedTreeSet extends TreeSet<SuggestedWordInfo> {
+ private final int mCapacity;
+ public BoundedTreeSet(final Comparator<SuggestedWordInfo> comparator, final int capacity) {
+ super(comparator);
+ mCapacity = capacity;
+ }
+
+ @Override
+ public boolean add(final SuggestedWordInfo e) {
+ if (size() < mCapacity) return super.add(e);
+ if (comparator().compare(e, last()) > 0) return false;
+ super.add(e);
+ pollLast(); // removes the last element
+ return true;
+ }
+
+ @Override
+ public boolean addAll(final Collection<? extends SuggestedWordInfo> e) {
+ if (null == e) return false;
+ return super.addAll(e);
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/ByteArrayDictBuffer.java b/java/src/com/android/inputmethod/latin/utils/ByteArrayDictBuffer.java
new file mode 100644
index 000000000..2028298f2
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/ByteArrayDictBuffer.java
@@ -0,0 +1,81 @@
+/*
+ * 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.utils;
+
+import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.DictBuffer;
+
+/**
+ * This class provides an implementation for the FusionDictionary buffer interface that is backed
+ * by a simpled byte array. It allows to create a binary dictionary in memory.
+ */
+public final class ByteArrayDictBuffer implements DictBuffer {
+ private byte[] mBuffer;
+ private int mPosition;
+
+ public ByteArrayDictBuffer(final byte[] buffer) {
+ mBuffer = buffer;
+ mPosition = 0;
+ }
+
+ @Override
+ public int readUnsignedByte() {
+ return mBuffer[mPosition++] & 0xFF;
+ }
+
+ @Override
+ public int readUnsignedShort() {
+ final int retval = readUnsignedByte();
+ return (retval << 8) + readUnsignedByte();
+ }
+
+ @Override
+ public int readUnsignedInt24() {
+ final int retval = readUnsignedShort();
+ return (retval << 8) + readUnsignedByte();
+ }
+
+ @Override
+ public int readInt() {
+ final int retval = readUnsignedShort();
+ return (retval << 16) + readUnsignedShort();
+ }
+
+ @Override
+ public int position() {
+ return mPosition;
+ }
+
+ @Override
+ public void position(int position) {
+ mPosition = position;
+ }
+
+ @Override
+ public void put(final byte b) {
+ mBuffer[mPosition++] = b;
+ }
+
+ @Override
+ public int limit() {
+ return mBuffer.length - 1;
+ }
+
+ @Override
+ public int capacity() {
+ return mBuffer.length;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/CapsModeUtils.java b/java/src/com/android/inputmethod/latin/utils/CapsModeUtils.java
new file mode 100644
index 000000000..60b24d5d5
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/CapsModeUtils.java
@@ -0,0 +1,274 @@
+/*
+ * 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.utils;
+
+import android.text.InputType;
+import android.text.TextUtils;
+
+import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.WordComposer;
+
+import java.util.Locale;
+
+public final class CapsModeUtils {
+ private CapsModeUtils() {
+ // This utility class is not publicly instantiable.
+ }
+
+ /**
+ * Apply an auto-caps mode to a string.
+ *
+ * This intentionally does NOT apply manual caps mode. It only changes the capitalization if
+ * the mode is one of the auto-caps modes.
+ * @param s The string to capitalize.
+ * @param capitalizeMode The mode in which to capitalize.
+ * @param locale The locale for capitalizing.
+ * @return The capitalized string.
+ */
+ public static String applyAutoCapsMode(final String s, final int capitalizeMode,
+ final Locale locale) {
+ if (WordComposer.CAPS_MODE_AUTO_SHIFT_LOCKED == capitalizeMode) {
+ return s.toUpperCase(locale);
+ } else if (WordComposer.CAPS_MODE_AUTO_SHIFTED == capitalizeMode) {
+ return StringUtils.capitalizeFirstCodePoint(s, locale);
+ } else {
+ return s;
+ }
+ }
+
+ /**
+ * Return whether a constant represents an auto-caps mode (either auto-shift or auto-shift-lock)
+ * @param mode The mode to test for
+ * @return true if this represents an auto-caps mode, false otherwise
+ */
+ public static boolean isAutoCapsMode(final int mode) {
+ return WordComposer.CAPS_MODE_AUTO_SHIFTED == mode
+ || WordComposer.CAPS_MODE_AUTO_SHIFT_LOCKED == mode;
+ }
+
+ private static boolean isPeriod(final int codePoint) {
+ // TODO: make this a resource.
+ return codePoint == Constants.CODE_PERIOD || codePoint == Constants.CODE_ARMENIAN_PERIOD;
+ }
+
+ /**
+ * Determine what caps mode should be in effect at the current offset in
+ * the text. Only the mode bits set in <var>reqModes</var> will be
+ * checked. Note that the caps mode flags here are explicitly defined
+ * to match those in {@link InputType}.
+ *
+ * This code is a straight copy of TextUtils.getCapsMode (modulo namespace and formatting
+ * issues). This will change in the future as we simplify the code for our use and fix bugs.
+ *
+ * @param cs The text that should be checked for caps modes.
+ * @param reqModes The modes to be checked: may be any combination of
+ * {@link TextUtils#CAP_MODE_CHARACTERS}, {@link TextUtils#CAP_MODE_WORDS}, and
+ * {@link TextUtils#CAP_MODE_SENTENCES}.
+ * @param locale The locale to consider for capitalization rules
+ * @param hasSpaceBefore Whether we should consider there is a space inserted at the end of cs
+ *
+ * @return Returns the actual capitalization modes that can be in effect
+ * at the current position, which is any combination of
+ * {@link TextUtils#CAP_MODE_CHARACTERS}, {@link TextUtils#CAP_MODE_WORDS}, and
+ * {@link TextUtils#CAP_MODE_SENTENCES}.
+ */
+ public static int getCapsMode(final CharSequence cs, final int reqModes, final Locale locale,
+ final boolean hasSpaceBefore) {
+ // Quick description of what we want to do:
+ // CAP_MODE_CHARACTERS is always on.
+ // CAP_MODE_WORDS is on if there is some whitespace before the cursor.
+ // CAP_MODE_SENTENCES is on if there is some whitespace before the cursor, and the end
+ // of a sentence just before that.
+ // We ignore opening parentheses and the like just before the cursor for purposes of
+ // finding whitespace for WORDS and SENTENCES modes.
+ // The end of a sentence ends with a period, question mark or exclamation mark. If it's
+ // a period, it also needs not to be an abbreviation, which means it also needs to either
+ // be immediately preceded by punctuation, or by a string of only letters with single
+ // periods interleaved.
+
+ // Step 1 : check for cap MODE_CHARACTERS. If it's looked for, it's always on.
+ if ((reqModes & (TextUtils.CAP_MODE_WORDS | TextUtils.CAP_MODE_SENTENCES)) == 0) {
+ // Here we are not looking for MODE_WORDS or MODE_SENTENCES, so since we already
+ // evaluated MODE_CHARACTERS, we can return.
+ return TextUtils.CAP_MODE_CHARACTERS & reqModes;
+ }
+
+ // Step 2 : Skip (ignore at the end of input) any opening punctuation. This includes
+ // opening parentheses, brackets, opening quotes, everything that *opens* a span of
+ // text in the linguistic sense. In RTL languages, this is still an opening sign, although
+ // it may look like a right parenthesis for example. We also include double quote and
+ // single quote since they aren't start punctuation in the unicode sense, but should still
+ // be skipped for English. TODO: does this depend on the language?
+ int i;
+ if (hasSpaceBefore) {
+ i = cs.length() + 1;
+ } else {
+ for (i = cs.length(); i > 0; i--) {
+ final char c = cs.charAt(i - 1);
+ if (c != Constants.CODE_DOUBLE_QUOTE && c != Constants.CODE_SINGLE_QUOTE
+ && Character.getType(c) != Character.START_PUNCTUATION) {
+ break;
+ }
+ }
+ }
+
+ // We are now on the character that precedes any starting punctuation, so in the most
+ // frequent case this will be whitespace or a letter, although it may occasionally be a
+ // start of line, or some symbol.
+
+ // Step 3 : Search for the start of a paragraph. From the starting point computed in step 2,
+ // we go back over any space or tab char sitting there. We find the start of a paragraph
+ // if the first char that's not a space or tab is a start of line (as in \n, start of text,
+ // or some other similar characters).
+ int j = i;
+ char prevChar = Constants.CODE_SPACE;
+ if (hasSpaceBefore) --j;
+ while (j > 0) {
+ prevChar = cs.charAt(j - 1);
+ if (!Character.isSpaceChar(prevChar) && prevChar != Constants.CODE_TAB) break;
+ j--;
+ }
+ if (j <= 0 || Character.isWhitespace(prevChar)) {
+ // There are only spacing chars between the start of the paragraph and the cursor,
+ // defined as a isWhitespace() char that is neither a isSpaceChar() nor a tab. Both
+ // MODE_WORDS and MODE_SENTENCES should be active.
+ return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS
+ | TextUtils.CAP_MODE_SENTENCES) & reqModes;
+ }
+ if (i == j) {
+ // If we don't have whitespace before index i, it means neither MODE_WORDS
+ // nor mode sentences should be on so we can return right away.
+ return TextUtils.CAP_MODE_CHARACTERS & reqModes;
+ }
+ if ((reqModes & TextUtils.CAP_MODE_SENTENCES) == 0) {
+ // Here we know we have whitespace before the cursor (if not, we returned in the above
+ // if i == j clause), so we need MODE_WORDS to be on. And we don't need to evaluate
+ // MODE_SENTENCES so we can return right away.
+ return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & reqModes;
+ }
+ // Please note that because of the reqModes & CAP_MODE_SENTENCES test a few lines above,
+ // we know that MODE_SENTENCES is being requested.
+
+ // Step 4 : Search for MODE_SENTENCES.
+ // English is a special case in that "American typography" rules, which are the most common
+ // in English, state that a sentence terminator immediately following a quotation mark
+ // should be swapped with it and de-duplicated (included in the quotation mark),
+ // e.g. <<Did he say, "let's go home?">>
+ // No other language has such a rule as far as I know, instead putting inside the quotation
+ // mark as the exact thing quoted and handling the surrounding punctuation independently,
+ // e.g. <<Did he say, "let's go home"?>>
+ // Hence, specifically for English, we treat this special case here.
+ if (Locale.ENGLISH.getLanguage().equals(locale.getLanguage())) {
+ for (; j > 0; j--) {
+ // Here we look to go over any closing punctuation. This is because in dominant
+ // variants of English, the final period is placed within double quotes and maybe
+ // other closing punctuation signs. This is generally not true in other languages.
+ final char c = cs.charAt(j - 1);
+ if (c != Constants.CODE_DOUBLE_QUOTE && c != Constants.CODE_SINGLE_QUOTE
+ && Character.getType(c) != Character.END_PUNCTUATION) {
+ break;
+ }
+ }
+ }
+
+ if (j <= 0) return TextUtils.CAP_MODE_CHARACTERS & reqModes;
+ char c = cs.charAt(--j);
+
+ // We found the next interesting chunk of text ; next we need to determine if it's the
+ // end of a sentence. If we have a question mark or an exclamation mark, it's the end of
+ // a sentence. If it's neither, the only remaining case is the period so we get the opposite
+ // case out of the way.
+ if (c == Constants.CODE_QUESTION_MARK || c == Constants.CODE_EXCLAMATION_MARK) {
+ return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_SENTENCES) & reqModes;
+ }
+ if (!isPeriod(c) || j <= 0) {
+ return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & reqModes;
+ }
+
+ // We found out that we have a period. We need to determine if this is a full stop or
+ // otherwise sentence-ending period, or an abbreviation like "e.g.". An abbreviation
+ // looks like (\w\.){2,}
+ // To find out, we will have a simple state machine with the following states :
+ // START, WORD, PERIOD, ABBREVIATION
+ // On START : (just before the first period)
+ // letter => WORD
+ // whitespace => end with no caps (it was a stand-alone period)
+ // otherwise => end with caps (several periods/symbols in a row)
+ // On WORD : (within the word just before the first period)
+ // letter => WORD
+ // period => PERIOD
+ // otherwise => end with caps (it was a word with a full stop at the end)
+ // On PERIOD : (period within a potential abbreviation)
+ // letter => LETTER
+ // otherwise => end with caps (it was not an abbreviation)
+ // On LETTER : (letter within a potential abbreviation)
+ // letter => LETTER
+ // period => PERIOD
+ // otherwise => end with no caps (it was an abbreviation)
+ // "Not an abbreviation" in the above chart essentially covers cases like "...yes.". This
+ // should capitalize.
+
+ final int START = 0;
+ final int WORD = 1;
+ final int PERIOD = 2;
+ final int LETTER = 3;
+ final int caps = (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS
+ | TextUtils.CAP_MODE_SENTENCES) & reqModes;
+ final int noCaps = (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & reqModes;
+ int state = START;
+ while (j > 0) {
+ c = cs.charAt(--j);
+ switch (state) {
+ case START:
+ if (Character.isLetter(c)) {
+ state = WORD;
+ } else if (Character.isWhitespace(c)) {
+ return noCaps;
+ } else {
+ return caps;
+ }
+ break;
+ case WORD:
+ if (Character.isLetter(c)) {
+ state = WORD;
+ } else if (isPeriod(c)) {
+ state = PERIOD;
+ } else {
+ return caps;
+ }
+ break;
+ case PERIOD:
+ if (Character.isLetter(c)) {
+ state = LETTER;
+ } else {
+ return caps;
+ }
+ break;
+ case LETTER:
+ if (Character.isLetter(c)) {
+ state = LETTER;
+ } else if (isPeriod(c)) {
+ state = PERIOD;
+ } else {
+ return noCaps;
+ }
+ }
+ }
+ // Here we arrived at the start of the line. This should behave exactly like whitespace.
+ return (START == state || LETTER == state) ? noCaps : caps;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java b/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java
new file mode 100644
index 000000000..cc25102ce
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java
@@ -0,0 +1,105 @@
+/*
+ * 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.utils;
+
+import android.util.SparseArray;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.WeakHashMap;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+public final class CollectionUtils {
+ private CollectionUtils() {
+ // This utility class is not publicly instantiable.
+ }
+
+ public static <K,V> HashMap<K,V> newHashMap() {
+ return new HashMap<K,V>();
+ }
+
+ public static <K, V> WeakHashMap<K, V> newWeakHashMap() {
+ return new WeakHashMap<K, V>();
+ }
+
+ public static <K,V> TreeMap<K,V> newTreeMap() {
+ return new TreeMap<K,V>();
+ }
+
+ public static <K, V> Map<K,V> newSynchronizedTreeMap() {
+ final TreeMap<K,V> treeMap = newTreeMap();
+ return Collections.synchronizedMap(treeMap);
+ }
+
+ public static <K,V> ConcurrentHashMap<K,V> newConcurrentHashMap() {
+ return new ConcurrentHashMap<K,V>();
+ }
+
+ public static <E> HashSet<E> newHashSet() {
+ return new HashSet<E>();
+ }
+
+ public static <E> TreeSet<E> newTreeSet() {
+ return new TreeSet<E>();
+ }
+
+ public static <E> ArrayList<E> newArrayList() {
+ return new ArrayList<E>();
+ }
+
+ public static <E> ArrayList<E> newArrayList(final int initialCapacity) {
+ return new ArrayList<E>(initialCapacity);
+ }
+
+ public static <E> ArrayList<E> newArrayList(final Collection<E> collection) {
+ return new ArrayList<E>(collection);
+ }
+
+ public static <E> LinkedList<E> newLinkedList() {
+ return new LinkedList<E>();
+ }
+
+ public static <E> CopyOnWriteArrayList<E> newCopyOnWriteArrayList() {
+ return new CopyOnWriteArrayList<E>();
+ }
+
+ public static <E> CopyOnWriteArrayList<E> newCopyOnWriteArrayList(
+ final Collection<E> collection) {
+ return new CopyOnWriteArrayList<E>(collection);
+ }
+
+ public static <E> CopyOnWriteArrayList<E> newCopyOnWriteArrayList(final E[] array) {
+ return new CopyOnWriteArrayList<E>(array);
+ }
+
+ public static <E> ArrayDeque<E> newArrayDeque() {
+ return new ArrayDeque<E>();
+ }
+
+ public static <E> SparseArray<E> newSparseArray() {
+ return new SparseArray<E>();
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/CompletionInfoUtils.java b/java/src/com/android/inputmethod/latin/utils/CompletionInfoUtils.java
new file mode 100644
index 000000000..5ccf0e079
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/CompletionInfoUtils.java
@@ -0,0 +1,43 @@
+/*
+ * 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.utils;
+
+import android.text.TextUtils;
+import android.view.inputmethod.CompletionInfo;
+
+import java.util.Arrays;
+
+/**
+ * Utilities to do various stuff with CompletionInfo.
+ */
+public class CompletionInfoUtils {
+ private CompletionInfoUtils() {
+ // This utility class is not publicly instantiable.
+ }
+
+ public static CompletionInfo[] removeNulls(final CompletionInfo[] src) {
+ int j = 0;
+ final CompletionInfo[] dst = new CompletionInfo[src.length];
+ for (int i = 0; i < src.length; ++i) {
+ if (null != src[i] && !TextUtils.isEmpty(src[i].getText())) {
+ dst[j] = src[i];
+ ++j;
+ }
+ }
+ return Arrays.copyOfRange(dst, 0, j);
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/CoordinateUtils.java b/java/src/com/android/inputmethod/latin/utils/CoordinateUtils.java
new file mode 100644
index 000000000..72f2cd2d9
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/CoordinateUtils.java
@@ -0,0 +1,49 @@
+/*
+ * 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.utils;
+
+public final class CoordinateUtils {
+ private static final int INDEX_X = 0;
+ private static final int INDEX_Y = 1;
+ private static final int ARRAY_SIZE = INDEX_Y + 1;
+
+ private CoordinateUtils() {
+ // This utility class is not publicly instantiable.
+ }
+
+ public static int[] newInstance() {
+ return new int[ARRAY_SIZE];
+ }
+
+ public static int x(final int[] coords) {
+ return coords[INDEX_X];
+ }
+
+ public static int y(final int[] coords) {
+ return coords[INDEX_Y];
+ }
+
+ public static void set(final int[] coords, final int x, final int y) {
+ coords[INDEX_X] = x;
+ coords[INDEX_Y] = y;
+ }
+
+ public static void copy(final int[] destination, final int[] source) {
+ destination[INDEX_X] = source[INDEX_X];
+ destination[INDEX_Y] = source[INDEX_Y];
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/CsvUtils.java b/java/src/com/android/inputmethod/latin/utils/CsvUtils.java
new file mode 100644
index 000000000..36b927eea
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/CsvUtils.java
@@ -0,0 +1,318 @@
+/*
+ * 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.utils;
+
+import com.android.inputmethod.annotations.UsedForTesting;
+
+import java.util.ArrayList;
+
+/**
+ * Utility methods for parsing and serializing Comma-Separated Values. The public APIs of this
+ * utility class are {@link #split(String)}, {@link #split(int,String)}, {@link #join(String...)},
+ * {@link #join(int,String...)}, and {@link #join(int,int[],String...)}.
+ *
+ * This class implements CSV parsing and serializing methods conforming to RFC 4180 with an
+ * exception:
+ * These methods can't handle new line code escaped in double quotes.
+ */
+@UsedForTesting
+public final class CsvUtils {
+ private CsvUtils() {
+ // This utility class is not publicly instantiable.
+ }
+
+ public static final int SPLIT_FLAGS_NONE = 0x0;
+ /**
+ * A flag for {@link #split(int,String)}. If this flag is specified, the method will trim
+ * spaces around fields before splitting. Note that this behavior doesn't conform to RFC 4180.
+ */
+ public static final int SPLIT_FLAGS_TRIM_SPACES = 0x1;
+
+ public static final int JOIN_FLAGS_NONE = 0x0;
+ /**
+ * A flag for {@link #join(int,String...)} and {@link #join(int,int[],String...)}. If this
+ * flag is specified, these methods surround each field with double quotes before joining.
+ */
+ public static final int JOIN_FLAGS_ALWAYS_QUOTED = 0x1;
+ /**
+ * A flag for {@link #join(int,String...)} and {@link #join(int,int[],String...)}. If this
+ * flag is specified, these methods add an extra space just after the comma separator. Note that
+ * this behavior doesn't conform to RFC 4180.
+ */
+ public static final int JOIN_FLAGS_EXTRA_SPACE = 0x2;
+
+ // 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 = '"';
+
+ @SuppressWarnings("serial")
+ public static class CsvParseException extends RuntimeException {
+ public CsvParseException(final String message) {
+ super(message);
+ }
+ }
+
+ /**
+ * Find the first non-space character in the text.
+ *
+ * @param text the text to be searched.
+ * @param fromIndex the index to start the search from, inclusive.
+ * @return the index of the first occurrence of the non-space character in the
+ * <code>text</code> that is greater than or equal to <code>fromIndex</code>, or the length of
+ * the <code>text</code> if the character does not occur.
+ */
+ private static int indexOfNonSpace(final String text, final int fromIndex) {
+ final int length = text.length();
+ if (fromIndex < 0 || fromIndex > length) {
+ throw new IllegalArgumentException("text=" + text + " fromIndex=" + fromIndex);
+ }
+ int index = fromIndex;
+ while (index < length && text.charAt(index) == SPACE) {
+ index++;
+ }
+ return index;
+ }
+
+ /**
+ * Find the last non-space character in the text.
+ *
+ * @param text the text to be searched.
+ * @param fromIndex the index to start the search from, exclusive.
+ * @param toIndex the index to end the search at, inclusive. Usually <code>toIndex</code>
+ * points a non-space character.
+ * @return the index of the last occurrence of the non-space character in the
+ * <code>text</code>, exclusive. It is less than <code>fromIndex</code> and greater than
+ * <code>toIndex</code>, or <code>toIndex</code> if the character does not occur.
+ */
+ private static int lastIndexOfNonSpace(final String text, final int fromIndex,
+ final int toIndex) {
+ if (toIndex < 0 || fromIndex > text.length() || fromIndex < toIndex) {
+ throw new IllegalArgumentException(
+ "text=" + text + " fromIndex=" + fromIndex + " toIndex=" + toIndex);
+ }
+ int index = fromIndex;
+ while (index > toIndex && text.charAt(index - 1) == SPACE) {
+ index--;
+ }
+ return index;
+ }
+
+ /**
+ * Find the index of a comma separator. The search takes account of quoted fields and escape
+ * quotes.
+ *
+ * @param text the text to be searched.
+ * @param fromIndex the index to start the search from, inclusive.
+ * @return the index of the comma separator, exclusive.
+ */
+ private static int indexOfSeparatorComma(final String text, final int fromIndex) {
+ final int length = text.length();
+ if (fromIndex < 0 || fromIndex > length) {
+ throw new IllegalArgumentException("text=" + text + " fromIndex=" + fromIndex);
+ }
+ final boolean isQuoted = (length - fromIndex > 0 && text.charAt(fromIndex) == QUOTE);
+ for (int index = fromIndex + (isQuoted ? 1 : 0); index < length; index++) {
+ final char c = text.charAt(index);
+ if (c == COMMA && !isQuoted) {
+ return index;
+ }
+ if (c == QUOTE) {
+ final int nextIndex = index + 1;
+ if (nextIndex < length && text.charAt(nextIndex) == QUOTE) {
+ // Quoted quote.
+ index = nextIndex;
+ continue;
+ }
+ // Closing quote.
+ final int endIndex = text.indexOf(COMMA, nextIndex);
+ return endIndex < 0 ? length : endIndex;
+ }
+ }
+ return length;
+ }
+
+ /**
+ * Removing any enclosing QUOTEs (U+0022), and convert any two consecutive QUOTEs into
+ * one QUOTE.
+ *
+ * @param text the CSV field text that may have enclosing QUOTEs and escaped QUOTE character.
+ * @return the text that has been removed enclosing quotes and converted two consecutive QUOTEs
+ * into one QUOTE.
+ */
+ @UsedForTesting
+ /* private */ static String unescapeField(final String text) {
+ StringBuilder sb = null;
+ final int length = text.length();
+ final boolean isQuoted = (length > 0 && text.charAt(0) == QUOTE);
+ int start = isQuoted ? 1 : 0;
+ int end = start;
+ while (start <= length && (end = text.indexOf(QUOTE, start)) >= start) {
+ final int nextIndex = end + 1;
+ if (nextIndex == length && isQuoted) {
+ // Closing quote.
+ break;
+ }
+ if (nextIndex < length && text.charAt(nextIndex) == QUOTE) {
+ if (!isQuoted) {
+ throw new CsvParseException("Escaped quote in text");
+ }
+ // Quoted quote.
+ if (sb == null) {
+ sb = new StringBuilder();
+ }
+ sb.append(text.substring(start, nextIndex));
+ start = nextIndex + 1;
+ } else {
+ throw new CsvParseException(
+ isQuoted ? "Raw quote in quoted text" : "Raw quote in text");
+ }
+ }
+ if (end < 0 && isQuoted) {
+ throw new CsvParseException("Unterminated quote");
+ }
+ if (end < 0) {
+ end = length;
+ }
+ if (sb != null && start < length) {
+ sb.append(text.substring(start, end));
+ }
+ return sb == null ? text.substring(start, end) : sb.toString();
+ }
+
+ /**
+ * Split the CSV text into fields. The leading and trailing spaces of the each field can be
+ * trimmed optionally.
+ *
+ * @param splitFlags flags for split behavior. {@link #SPLIT_FLAGS_TRIM_SPACES} will trim
+ * spaces around each fields.
+ * @param line the text of CSV fields.
+ * @return the array of unescaped CVS fields.
+ * @throws CsvParseException
+ */
+ @UsedForTesting
+ public static String[] split(final int splitFlags, final String line) throws CsvParseException {
+ final boolean trimSpaces = (splitFlags & SPLIT_FLAGS_TRIM_SPACES) != 0;
+ final ArrayList<String> fields = CollectionUtils.newArrayList();
+ final int length = line.length();
+ int start = 0;
+ do {
+ final int csvStart = trimSpaces ? indexOfNonSpace(line, start) : start;
+ final int end = indexOfSeparatorComma(line, csvStart);
+ final int csvEnd = trimSpaces ? lastIndexOfNonSpace(line, end, csvStart) : end;
+ final String csvText = unescapeField(line.substring(csvStart, csvEnd));
+ fields.add(csvText);
+ start = end + 1;
+ } while (start <= length);
+ return fields.toArray(new String[fields.size()]);
+ }
+
+ @UsedForTesting
+ public static String[] split(final String line) throws CsvParseException {
+ return split(SPLIT_FLAGS_NONE, line);
+ }
+
+ /**
+ * Convert the raw CSV field text to the escaped text. It adds enclosing QUOTEs (U+0022) if the
+ * raw value contains any QUOTE or comma. Also it converts any QUOTE character into two
+ * consecutive QUOTE characters.
+ *
+ * @param text the raw CSV field text to be escaped.
+ * @param alwaysQuoted true if the escaped text should always be enclosed by QUOTEs.
+ * @return the escaped text.
+ */
+ @UsedForTesting
+ /* private */ static String escapeField(final String text, final boolean alwaysQuoted) {
+ StringBuilder sb = null;
+ boolean needsQuoted = alwaysQuoted;
+ final int length = text.length();
+ int indexToBeAppended = 0;
+ for (int index = indexToBeAppended; index < length; index++) {
+ final char c = text.charAt(index);
+ if (c == COMMA) {
+ needsQuoted = true;
+ } else if (c == QUOTE) {
+ needsQuoted = true;
+ if (sb == null) {
+ sb = new StringBuilder();
+ }
+ sb.append(text.substring(indexToBeAppended, index));
+ indexToBeAppended = index + 1;
+ sb.append(QUOTE); // escaping quote.
+ sb.append(QUOTE); // escaped quote.
+ }
+ }
+ if (sb != null && indexToBeAppended < length) {
+ sb.append(text.substring(indexToBeAppended));
+ }
+ final String escapedText = (sb == null) ? text : sb.toString();
+ return needsQuoted ? QUOTE + escapedText + QUOTE : escapedText;
+ }
+
+ private static final String SPACES = " ";
+
+ private static void padToColumn(final StringBuilder sb, final int column) {
+ int padding;
+ while ((padding = column - sb.length()) > 0) {
+ final String spaces = SPACES.substring(0, Math.min(padding, SPACES.length()));
+ sb.append(spaces);
+ }
+ }
+
+ /**
+ * Join CSV text fields with comma. The column positions of the fields can be specified
+ * optionally. Surround each fields with double quotes before joining.
+ *
+ * @param joinFlags flags for join behavior. {@link #JOIN_FLAGS_EXTRA_SPACE} will add an extra
+ * space after each comma separator. {@link #JOIN_FLAGS_ALWAYS_QUOTED} will always add
+ * surrounding quotes to each element.
+ * @param columnPositions the array of column positions of the fields. It can be shorter than
+ * <code>fields</code> or null. Note that specifying the array column positions of the fields
+ * doesn't conform to RFC 4180.
+ * @param fields the CSV text fields.
+ * @return the string of the joined and escaped <code>fields</code>.
+ */
+ @UsedForTesting
+ public static String join(final int joinFlags, final int columnPositions[],
+ final String... fields) {
+ final boolean alwaysQuoted = (joinFlags & JOIN_FLAGS_ALWAYS_QUOTED) != 0;
+ final String separator = COMMA + ((joinFlags & JOIN_FLAGS_EXTRA_SPACE) != 0 ? " " : "");
+ final StringBuilder sb = new StringBuilder();
+ for (int index = 0; index < fields.length; index++) {
+ if (index > 0) {
+ sb.append(separator);
+ }
+ if (columnPositions != null && index < columnPositions.length) {
+ padToColumn(sb, columnPositions[index]);
+ }
+ final String escapedText = escapeField(fields[index], alwaysQuoted);
+ sb.append(escapedText);
+ }
+ return sb.toString();
+ }
+
+ @UsedForTesting
+ public static String join(final int joinFlags, final String... fields) {
+ return join(joinFlags, null, fields);
+ }
+
+ @UsedForTesting
+ public static String join(final String... fields) {
+ return join(JOIN_FLAGS_NONE, null, fields);
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/DebugLogUtils.java b/java/src/com/android/inputmethod/latin/utils/DebugLogUtils.java
new file mode 100644
index 000000000..ac654fa65
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/DebugLogUtils.java
@@ -0,0 +1,115 @@
+/*
+ * 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.utils;
+
+import android.util.Log;
+
+import com.android.inputmethod.latin.LatinImeLogger;
+
+/**
+ * A class for logging and debugging utility methods.
+ */
+public final class DebugLogUtils {
+ private final static String TAG = DebugLogUtils.class.getSimpleName();
+ private final static boolean sDBG = LatinImeLogger.sDBG;
+
+ /**
+ * Calls .toString() on its non-null argument or returns "null"
+ * @param o the object to convert to a string
+ * @return the result of .toString() or null
+ */
+ public static String s(final Object o) {
+ return null == o ? "null" : o.toString();
+ }
+
+ /**
+ * Get the string representation of the current stack trace, for debugging purposes.
+ * @return a readable, carriage-return-separated string for the current stack trace.
+ */
+ public static String getStackTrace() {
+ return getStackTrace(Integer.MAX_VALUE - 1);
+ }
+
+ /**
+ * Get the string representation of the current stack trace, for debugging purposes.
+ * @param limit the maximum number of stack frames to be returned.
+ * @return a readable, carriage-return-separated string for the current stack trace.
+ */
+ public static String getStackTrace(final int limit) {
+ final StringBuilder sb = new StringBuilder();
+ try {
+ throw new RuntimeException();
+ } catch (final RuntimeException e) {
+ final StackTraceElement[] frames = e.getStackTrace();
+ // Start at 1 because the first frame is here and we don't care about it
+ for (int j = 1; j < frames.length && j < limit + 1; ++j) {
+ sb.append(frames[j].toString() + "\n");
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Get the stack trace contained in an exception as a human-readable string.
+ * @param t the throwable
+ * @return the human-readable stack trace
+ */
+ public static String getStackTrace(final Throwable t) {
+ final StringBuilder sb = new StringBuilder();
+ final StackTraceElement[] frames = t.getStackTrace();
+ for (int j = 0; j < frames.length; ++j) {
+ sb.append(frames[j].toString() + "\n");
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Helper log method to ease null-checks and adding spaces.
+ *
+ * This sends all arguments to the log, separated by spaces. Any null argument is converted
+ * to the "null" string. It uses a very visible tag and log level for debugging purposes.
+ *
+ * @param args the stuff to send to the log
+ */
+ public static void l(final Object... args) {
+ if (!sDBG) return;
+ final StringBuilder sb = new StringBuilder();
+ for (final Object o : args) {
+ sb.append(s(o).toString());
+ sb.append(" ");
+ }
+ Log.e(TAG, sb.toString());
+ }
+
+ /**
+ * Helper log method to put stuff in red.
+ *
+ * This does the same as #l but prints in red
+ *
+ * @param args the stuff to send to the log
+ */
+ public static void r(final Object... args) {
+ if (!sDBG) return;
+ final StringBuilder sb = new StringBuilder("\u001B[31m");
+ for (final Object o : args) {
+ sb.append(s(o).toString());
+ sb.append(" ");
+ }
+ sb.append("\u001B[0m");
+ Log.e(TAG, sb.toString());
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java b/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java
new file mode 100644
index 000000000..021bf0825
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java
@@ -0,0 +1,358 @@
+/*
+ * 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.utils;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.res.AssetManager;
+import android.content.res.Resources;
+import android.util.Log;
+
+import com.android.inputmethod.latin.AssetFileAddress;
+import com.android.inputmethod.latin.BinaryDictionaryGetter;
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.makedict.BinaryDictIOUtils;
+import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * This class encapsulates the logic for the Latin-IME side of dictionary information management.
+ */
+public class DictionaryInfoUtils {
+ private static final String TAG = DictionaryInfoUtils.class.getSimpleName();
+ private static final String RESOURCE_PACKAGE_NAME = R.class.getPackage().getName();
+ private static final String DEFAULT_MAIN_DICT = "main";
+ private static final String MAIN_DICT_PREFIX = "main_";
+ // 6 digits - unicode is limited to 21 bits
+ private static final int MAX_HEX_DIGITS_FOR_CODEPOINT = 6;
+
+ public static class DictionaryInfo {
+ private static final String LOCALE_COLUMN = "locale";
+ private static final String WORDLISTID_COLUMN = "id";
+ private static final String LOCAL_FILENAME_COLUMN = "filename";
+ private static final String DESCRIPTION_COLUMN = "description";
+ private static final String DATE_COLUMN = "date";
+ private static final String FILESIZE_COLUMN = "filesize";
+ private static final String VERSION_COLUMN = "version";
+ public final String mId;
+ public final Locale mLocale;
+ public final String mDescription;
+ public final AssetFileAddress mFileAddress;
+ public final int mVersion;
+ public DictionaryInfo(final String id, final Locale locale, final String description,
+ final AssetFileAddress fileAddress, final int version) {
+ mId = id;
+ mLocale = locale;
+ mDescription = description;
+ mFileAddress = fileAddress;
+ mVersion = version;
+ }
+ public ContentValues toContentValues() {
+ final ContentValues values = new ContentValues();
+ values.put(WORDLISTID_COLUMN, mId);
+ values.put(LOCALE_COLUMN, mLocale.toString());
+ values.put(DESCRIPTION_COLUMN, mDescription);
+ values.put(LOCAL_FILENAME_COLUMN, mFileAddress.mFilename);
+ values.put(DATE_COLUMN, TimeUnit.MILLISECONDS.toSeconds(
+ new File(mFileAddress.mFilename).lastModified()));
+ values.put(FILESIZE_COLUMN, mFileAddress.mLength);
+ values.put(VERSION_COLUMN, mVersion);
+ return values;
+ }
+ }
+
+ private DictionaryInfoUtils() {
+ // Private constructor to forbid instantation of this helper class.
+ }
+
+ /**
+ * Returns whether we may want to use this character as part of a file name.
+ *
+ * This basically only accepts ascii letters and numbers, and rejects everything else.
+ */
+ private static boolean isFileNameCharacter(int codePoint) {
+ if (codePoint >= 0x30 && codePoint <= 0x39) return true; // Digit
+ if (codePoint >= 0x41 && codePoint <= 0x5A) return true; // Uppercase
+ if (codePoint >= 0x61 && codePoint <= 0x7A) return true; // Lowercase
+ return codePoint == '_'; // Underscore
+ }
+
+ /**
+ * Escapes a string for any characters that may be suspicious for a file or directory name.
+ *
+ * Concretely this does a sort of URL-encoding except it will encode everything that's not
+ * alphanumeric or underscore. (true URL-encoding leaves alone characters like '*', which
+ * we cannot allow here)
+ */
+ // TODO: create a unit test for this method
+ public static String replaceFileNameDangerousCharacters(final String name) {
+ // This assumes '%' is fully available as a non-separator, normal
+ // character in a file name. This is probably true for all file systems.
+ final StringBuilder sb = new StringBuilder();
+ final int nameLength = name.length();
+ for (int i = 0; i < nameLength; i = name.offsetByCodePoints(i, 1)) {
+ final int codePoint = name.codePointAt(i);
+ if (DictionaryInfoUtils.isFileNameCharacter(codePoint)) {
+ sb.appendCodePoint(codePoint);
+ } else {
+ sb.append(String.format((Locale)null, "%%%1$0" + MAX_HEX_DIGITS_FOR_CODEPOINT + "x",
+ codePoint));
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Helper method to get the top level cache directory.
+ */
+ private static String getWordListCacheDirectory(final Context context) {
+ return context.getFilesDir() + File.separator + "dicts";
+ }
+
+ /**
+ * Helper method to get the top level temp directory.
+ */
+ public static String getWordListTempDirectory(final Context context) {
+ return context.getFilesDir() + File.separator + "tmp";
+ }
+
+ /**
+ * Reverse escaping done by replaceFileNameDangerousCharacters.
+ */
+ public static String getWordListIdFromFileName(final String fname) {
+ final StringBuilder sb = new StringBuilder();
+ final int fnameLength = fname.length();
+ for (int i = 0; i < fnameLength; i = fname.offsetByCodePoints(i, 1)) {
+ final int codePoint = fname.codePointAt(i);
+ if ('%' != codePoint) {
+ sb.appendCodePoint(codePoint);
+ } else {
+ // + 1 to pass the % sign
+ final int encodedCodePoint = Integer.parseInt(
+ fname.substring(i + 1, i + 1 + MAX_HEX_DIGITS_FOR_CODEPOINT), 16);
+ i += MAX_HEX_DIGITS_FOR_CODEPOINT;
+ sb.appendCodePoint(encodedCodePoint);
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Helper method to the list of cache directories, one for each distinct locale.
+ */
+ public static File[] getCachedDirectoryList(final Context context) {
+ return new File(DictionaryInfoUtils.getWordListCacheDirectory(context)).listFiles();
+ }
+
+ /**
+ * Returns the category for a given file name.
+ *
+ * This parses the file name, extracts the category, and returns it. See
+ * {@link #getMainDictId(Locale)} and {@link #isMainWordListId(String)}.
+ * @return The category as a string or null if it can't be found in the file name.
+ */
+ public static String getCategoryFromFileName(final String fileName) {
+ final String id = getWordListIdFromFileName(fileName);
+ final String[] idArray = id.split(BinaryDictionaryGetter.ID_CATEGORY_SEPARATOR);
+ // An id is supposed to be in format category:locale, so splitting on the separator
+ // should yield a 2-elements array
+ if (2 != idArray.length) return null;
+ return idArray[0];
+ }
+
+ /**
+ * Find out the cache directory associated with a specific locale.
+ */
+ private static String getCacheDirectoryForLocale(final String locale, final Context context) {
+ final String relativeDirectoryName = replaceFileNameDangerousCharacters(locale);
+ final String absoluteDirectoryName = getWordListCacheDirectory(context) + File.separator
+ + relativeDirectoryName;
+ final File directory = new File(absoluteDirectoryName);
+ if (!directory.exists()) {
+ if (!directory.mkdirs()) {
+ Log.e(TAG, "Could not create the directory for locale" + locale);
+ }
+ }
+ return absoluteDirectoryName;
+ }
+
+ /**
+ * Generates a file name for the id and locale passed as an argument.
+ *
+ * In the current implementation the file name returned will always be unique for
+ * any id/locale pair, but please do not expect that the id can be the same for
+ * different dictionaries with different locales. An id should be unique for any
+ * dictionary.
+ * The file name is pretty much an URL-encoded version of the id inside a directory
+ * named like the locale, except it will also escape characters that look dangerous
+ * to some file systems.
+ * @param id the id of the dictionary for which to get a file name
+ * @param locale the locale for which to get the file name as a string
+ * @param context the context to use for getting the directory
+ * @return the name of the file to be created
+ */
+ public static String getCacheFileName(String id, String locale, Context context) {
+ final String fileName = replaceFileNameDangerousCharacters(id);
+ return getCacheDirectoryForLocale(locale, context) + File.separator + fileName;
+ }
+
+ public static boolean isMainWordListId(final String id) {
+ final String[] idArray = id.split(BinaryDictionaryGetter.ID_CATEGORY_SEPARATOR);
+ // An id is supposed to be in format category:locale, so splitting on the separator
+ // should yield a 2-elements array
+ if (2 != idArray.length) return false;
+ return BinaryDictionaryGetter.MAIN_DICTIONARY_CATEGORY.equals(idArray[0]);
+ }
+
+ /**
+ * Helper method to return a dictionary res id for a locale, or 0 if none.
+ * @param locale dictionary locale
+ * @return main dictionary resource id
+ */
+ public static int getMainDictionaryResourceIdIfAvailableForLocale(final Resources res,
+ final Locale locale) {
+ int resId;
+ // Try to find main_language_country dictionary.
+ if (!locale.getCountry().isEmpty()) {
+ final String dictLanguageCountry =
+ MAIN_DICT_PREFIX + locale.toString().toLowerCase(Locale.ROOT);
+ if ((resId = res.getIdentifier(
+ dictLanguageCountry, "raw", RESOURCE_PACKAGE_NAME)) != 0) {
+ return resId;
+ }
+ }
+
+ // Try to find main_language dictionary.
+ final String dictLanguage = MAIN_DICT_PREFIX + locale.getLanguage();
+ if ((resId = res.getIdentifier(dictLanguage, "raw", RESOURCE_PACKAGE_NAME)) != 0) {
+ return resId;
+ }
+
+ // Not found, return 0
+ return 0;
+ }
+
+ /**
+ * Returns a main dictionary resource id
+ * @param locale dictionary locale
+ * @return main dictionary resource id
+ */
+ public static int getMainDictionaryResourceId(final Resources res, final Locale locale) {
+ int resourceId = getMainDictionaryResourceIdIfAvailableForLocale(res, locale);
+ if (0 != resourceId) return resourceId;
+ return res.getIdentifier(DEFAULT_MAIN_DICT, "raw", RESOURCE_PACKAGE_NAME);
+ }
+
+ /**
+ * Returns the id associated with the main word list for a specified locale.
+ *
+ * Word lists stored in Android Keyboard's resources are referred to as the "main"
+ * word lists. Since they can be updated like any other list, we need to assign a
+ * unique ID to them. This ID is just the name of the language (locale-wise) they
+ * are for, and this method returns this ID.
+ */
+ public static String getMainDictId(final Locale locale) {
+ // This works because we don't include by default different dictionaries for
+ // different countries. This actually needs to return the id that we would
+ // like to use for word lists included in resources, and the following is okay.
+ return BinaryDictionaryGetter.MAIN_DICTIONARY_CATEGORY +
+ BinaryDictionaryGetter.ID_CATEGORY_SEPARATOR + locale.getLanguage().toString();
+ }
+
+ public static FileHeader getDictionaryFileHeaderOrNull(final File file) {
+ return BinaryDictIOUtils.getDictionaryFileHeaderOrNull(file, 0, file.length());
+ }
+
+ private static DictionaryInfo createDictionaryInfoFromFileAddress(
+ final AssetFileAddress fileAddress) {
+ final FileHeader header = BinaryDictIOUtils.getDictionaryFileHeaderOrNull(
+ new File(fileAddress.mFilename), fileAddress.mOffset, fileAddress.mLength);
+ final String id = header.getId();
+ final Locale locale = LocaleUtils.constructLocaleFromString(header.getLocaleString());
+ final String description = header.getDescription();
+ final String version = header.getVersion();
+ return new DictionaryInfo(id, locale, description, fileAddress, Integer.parseInt(version));
+ }
+
+ private static void addOrUpdateDictInfo(final ArrayList<DictionaryInfo> dictList,
+ final DictionaryInfo newElement) {
+ final Iterator<DictionaryInfo> iter = dictList.iterator();
+ while (iter.hasNext()) {
+ final DictionaryInfo thisDictInfo = iter.next();
+ if (thisDictInfo.mLocale.equals(newElement.mLocale)) {
+ if (newElement.mVersion <= thisDictInfo.mVersion) {
+ return;
+ }
+ iter.remove();
+ }
+ }
+ dictList.add(newElement);
+ }
+
+ public static ArrayList<DictionaryInfo> getCurrentDictionaryFileNameAndVersionInfo(
+ final Context context) {
+ final ArrayList<DictionaryInfo> dictList = CollectionUtils.newArrayList();
+
+ // Retrieve downloaded dictionaries
+ final File[] directoryList = getCachedDirectoryList(context);
+ if (null != directoryList) {
+ for (final File directory : directoryList) {
+ final String localeString = getWordListIdFromFileName(directory.getName());
+ File[] dicts = BinaryDictionaryGetter.getCachedWordLists(localeString, context);
+ for (final File dict : dicts) {
+ final String wordListId = getWordListIdFromFileName(dict.getName());
+ if (!DictionaryInfoUtils.isMainWordListId(wordListId)) continue;
+ final Locale locale = LocaleUtils.constructLocaleFromString(localeString);
+ final AssetFileAddress fileAddress = AssetFileAddress.makeFromFile(dict);
+ final DictionaryInfo dictionaryInfo =
+ createDictionaryInfoFromFileAddress(fileAddress);
+ // Protect against cases of a less-specific dictionary being found, like an
+ // en dictionary being used for an en_US locale. In this case, the en dictionary
+ // should be used for en_US but discounted for listing purposes.
+ if (!dictionaryInfo.mLocale.equals(locale)) continue;
+ addOrUpdateDictInfo(dictList, dictionaryInfo);
+ }
+ }
+ }
+
+ // Retrieve files from assets
+ final Resources resources = context.getResources();
+ final AssetManager assets = resources.getAssets();
+ for (final String localeString : assets.getLocales()) {
+ final Locale locale = LocaleUtils.constructLocaleFromString(localeString);
+ final int resourceId =
+ DictionaryInfoUtils.getMainDictionaryResourceIdIfAvailableForLocale(
+ context.getResources(), locale);
+ if (0 == resourceId) continue;
+ final AssetFileAddress fileAddress =
+ BinaryDictionaryGetter.loadFallbackResource(context, resourceId);
+ final DictionaryInfo dictionaryInfo = createDictionaryInfoFromFileAddress(fileAddress);
+ // Protect against cases of a less-specific dictionary being found, like an
+ // en dictionary being used for an en_US locale. In this case, the en dictionary
+ // should be used for en_US but discounted for listing purposes.
+ if (!dictionaryInfo.mLocale.equals(locale)) continue;
+ addOrUpdateDictInfo(dictList, dictionaryInfo);
+ }
+
+ return dictList;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/FeedbackUtils.java b/java/src/com/android/inputmethod/latin/utils/FeedbackUtils.java
new file mode 100644
index 000000000..ec7eaf4a0
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/FeedbackUtils.java
@@ -0,0 +1,37 @@
+/*
+ * 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.utils;
+
+import android.content.Context;
+import android.content.Intent;
+
+public class FeedbackUtils {
+ public static boolean isFeedbackFormSupported() {
+ return false;
+ }
+
+ public static void showFeedbackForm(Context context) {
+ }
+
+ public static int getAboutKeyboardTitleResId() {
+ return 0;
+ }
+
+ public static Intent getAboutKeyboardIntent(Context context) {
+ return null;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/FileTransforms.java b/java/src/com/android/inputmethod/latin/utils/FileTransforms.java
new file mode 100644
index 000000000..9f4584ec9
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/FileTransforms.java
@@ -0,0 +1,38 @@
+/*
+ * 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.utils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.zip.GZIPInputStream;
+
+public final class FileTransforms {
+ public static OutputStream getCryptedStream(OutputStream out) {
+ // Crypt the stream.
+ return out;
+ }
+
+ public static InputStream getDecryptedStream(InputStream in) {
+ // Decrypt the stream.
+ return in;
+ }
+
+ public static InputStream getUncompressedStream(InputStream in) throws IOException {
+ return new GZIPInputStream(in);
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/InputTypeUtils.java b/java/src/com/android/inputmethod/latin/utils/InputTypeUtils.java
new file mode 100644
index 000000000..19cd34011
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/InputTypeUtils.java
@@ -0,0 +1,117 @@
+/*
+ * 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.utils;
+
+import android.text.InputType;
+import android.view.inputmethod.EditorInfo;
+
+public final class InputTypeUtils implements InputType {
+ private static final int WEB_TEXT_PASSWORD_INPUT_TYPE =
+ TYPE_CLASS_TEXT | TYPE_TEXT_VARIATION_WEB_PASSWORD;
+ private static final int WEB_TEXT_EMAIL_ADDRESS_INPUT_TYPE =
+ TYPE_CLASS_TEXT | TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS;
+ private static final int NUMBER_PASSWORD_INPUT_TYPE =
+ TYPE_CLASS_NUMBER | TYPE_NUMBER_VARIATION_PASSWORD;
+ private static final int TEXT_PASSWORD_INPUT_TYPE =
+ TYPE_CLASS_TEXT | TYPE_TEXT_VARIATION_PASSWORD;
+ private static final int TEXT_VISIBLE_PASSWORD_INPUT_TYPE =
+ TYPE_CLASS_TEXT | TYPE_TEXT_VARIATION_VISIBLE_PASSWORD;
+ private static final int[] SUPPRESSING_AUTO_SPACES_FIELD_VARIATION = {
+ InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS,
+ InputType.TYPE_TEXT_VARIATION_PASSWORD,
+ InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD,
+ InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD };
+ public static final int IME_ACTION_CUSTOM_LABEL = EditorInfo.IME_MASK_ACTION + 1;
+
+ private InputTypeUtils() {
+ // This utility class is not publicly instantiable.
+ }
+
+ private static boolean isWebEditTextInputType(final int inputType) {
+ return inputType == (TYPE_CLASS_TEXT | TYPE_TEXT_VARIATION_WEB_EDIT_TEXT);
+ }
+
+ private static boolean isWebPasswordInputType(final int inputType) {
+ return WEB_TEXT_PASSWORD_INPUT_TYPE != 0
+ && inputType == WEB_TEXT_PASSWORD_INPUT_TYPE;
+ }
+
+ private static boolean isWebEmailAddressInputType(final int inputType) {
+ return WEB_TEXT_EMAIL_ADDRESS_INPUT_TYPE != 0
+ && inputType == WEB_TEXT_EMAIL_ADDRESS_INPUT_TYPE;
+ }
+
+ private static boolean isNumberPasswordInputType(final int inputType) {
+ return NUMBER_PASSWORD_INPUT_TYPE != 0
+ && inputType == NUMBER_PASSWORD_INPUT_TYPE;
+ }
+
+ private static boolean isTextPasswordInputType(final int inputType) {
+ return inputType == TEXT_PASSWORD_INPUT_TYPE;
+ }
+
+ private static boolean isWebEmailAddressVariation(int variation) {
+ return variation == TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS;
+ }
+
+ public static boolean isEmailVariation(final int variation) {
+ return variation == TYPE_TEXT_VARIATION_EMAIL_ADDRESS
+ || isWebEmailAddressVariation(variation);
+ }
+
+ public static boolean isWebInputType(final int inputType) {
+ final int maskedInputType =
+ inputType & (TYPE_MASK_CLASS | TYPE_MASK_VARIATION);
+ return isWebEditTextInputType(maskedInputType) || isWebPasswordInputType(maskedInputType)
+ || isWebEmailAddressInputType(maskedInputType);
+ }
+
+ // Please refer to TextView.isPasswordInputType
+ public static boolean isPasswordInputType(final int inputType) {
+ final int maskedInputType =
+ inputType & (TYPE_MASK_CLASS | TYPE_MASK_VARIATION);
+ return isTextPasswordInputType(maskedInputType) || isWebPasswordInputType(maskedInputType)
+ || isNumberPasswordInputType(maskedInputType);
+ }
+
+ // Please refer to TextView.isVisiblePasswordInputType
+ public static boolean isVisiblePasswordInputType(final int inputType) {
+ final int maskedInputType =
+ inputType & (TYPE_MASK_CLASS | TYPE_MASK_VARIATION);
+ return maskedInputType == TEXT_VISIBLE_PASSWORD_INPUT_TYPE;
+ }
+
+ public static boolean isAutoSpaceFriendlyType(final int inputType) {
+ if (TYPE_CLASS_TEXT != (TYPE_MASK_CLASS & inputType)) return false;
+ final int variation = TYPE_MASK_VARIATION & inputType;
+ for (final int fieldVariation : SUPPRESSING_AUTO_SPACES_FIELD_VARIATION) {
+ if (variation == fieldVariation) return false;
+ }
+ return true;
+ }
+
+ public static int getImeOptionsActionIdFromEditorInfo(final EditorInfo editorInfo) {
+ if ((editorInfo.imeOptions & EditorInfo.IME_FLAG_NO_ENTER_ACTION) != 0) {
+ return EditorInfo.IME_ACTION_NONE;
+ } else if (editorInfo.actionLabel != null) {
+ return IME_ACTION_CUSTOM_LABEL;
+ } else {
+ // Note: this is different from editorInfo.actionId, hence "ImeOptionsActionId"
+ return editorInfo.imeOptions & EditorInfo.IME_MASK_ACTION;
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/IntentUtils.java b/java/src/com/android/inputmethod/latin/utils/IntentUtils.java
new file mode 100644
index 000000000..ea0168117
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/IntentUtils.java
@@ -0,0 +1,45 @@
+/*
+ * 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.utils;
+
+import android.content.Intent;
+import android.text.TextUtils;
+
+public final class IntentUtils {
+ private static final String EXTRA_INPUT_METHOD_ID = "input_method_id";
+ // TODO: Can these be constants instead of literal String constants?
+ private static final String INPUT_METHOD_SUBTYPE_SETTINGS =
+ "android.settings.INPUT_METHOD_SUBTYPE_SETTINGS";
+
+ private IntentUtils() {
+ // This utility class is not publicly instantiable.
+ }
+
+ public static Intent getInputLanguageSelectionIntent(final String inputMethodId,
+ final int flagsForSubtypeSettings) {
+ // Refer to android.provider.Settings.ACTION_INPUT_METHOD_SUBTYPE_SETTINGS
+ final String action = INPUT_METHOD_SUBTYPE_SETTINGS;
+ final Intent intent = new Intent(action);
+ if (!TextUtils.isEmpty(inputMethodId)) {
+ intent.putExtra(EXTRA_INPUT_METHOD_ID, inputMethodId);
+ }
+ if (flagsForSubtypeSettings > 0) {
+ intent.setFlags(flagsForSubtypeSettings);
+ }
+ return intent;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/JniUtils.java b/java/src/com/android/inputmethod/latin/utils/JniUtils.java
new file mode 100644
index 000000000..e7fdafaeb
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/JniUtils.java
@@ -0,0 +1,41 @@
+/*
+ * 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.utils;
+
+import android.util.Log;
+
+import com.android.inputmethod.latin.define.JniLibName;
+
+public final class JniUtils {
+ private static final String TAG = JniUtils.class.getSimpleName();
+
+ static {
+ try {
+ System.loadLibrary(JniLibName.JNI_LIB_NAME);
+ } catch (UnsatisfiedLinkError ule) {
+ Log.e(TAG, "Could not load native library " + JniLibName.JNI_LIB_NAME, ule);
+ }
+ }
+
+ private JniUtils() {
+ // This utility class is not publicly instantiable.
+ }
+
+ public static void loadNativeLibrary() {
+ // Ensures the static initializer is called
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/LatinImeLoggerUtils.java b/java/src/com/android/inputmethod/latin/utils/LatinImeLoggerUtils.java
new file mode 100644
index 000000000..e958a7e71
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/LatinImeLoggerUtils.java
@@ -0,0 +1,77 @@
+/*
+ * 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.utils;
+
+import android.text.TextUtils;
+
+import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.LatinImeLogger;
+import com.android.inputmethod.latin.WordComposer;
+
+public final class LatinImeLoggerUtils {
+ private LatinImeLoggerUtils() {
+ // This utility class is not publicly instantiable.
+ }
+
+ public static void onNonSeparator(final char code, final int x, final int y) {
+ UserLogRingCharBuffer.getInstance().push(code, x, y);
+ LatinImeLogger.logOnInputChar();
+ }
+
+ public static void onSeparator(final int code, final int x, final int y) {
+ // Helper method to log a single code point separator
+ // TODO: cache this mapping of a code point to a string in a sparse array in StringUtils
+ onSeparator(new String(new int[]{code}, 0, 1), x, y);
+ }
+
+ public static void onSeparator(final String separator, final int x, final int y) {
+ final int length = separator.length();
+ for (int i = 0; i < length; i = Character.offsetByCodePoints(separator, i, 1)) {
+ int codePoint = Character.codePointAt(separator, i);
+ // TODO: accept code points
+ UserLogRingCharBuffer.getInstance().push((char)codePoint, x, y);
+ }
+ LatinImeLogger.logOnInputSeparator();
+ }
+
+ public static void onAutoCorrection(final String typedWord, final String correctedWord,
+ final String separatorString, final WordComposer wordComposer) {
+ final boolean isBatchMode = wordComposer.isBatchMode();
+ if (!isBatchMode && TextUtils.isEmpty(typedWord)) {
+ return;
+ }
+ // TODO: this fails when the separator is more than 1 code point long, but
+ // the backend can't handle it yet. The only case when this happens is with
+ // smileys and other multi-character keys.
+ final int codePoint = TextUtils.isEmpty(separatorString) ? Constants.NOT_A_CODE
+ : separatorString.codePointAt(0);
+ if (!isBatchMode) {
+ LatinImeLogger.logOnAutoCorrectionForTyping(typedWord, correctedWord, codePoint);
+ } else {
+ if (!TextUtils.isEmpty(correctedWord)) {
+ // We must make sure that InputPointer contains only the relative timestamps,
+ // not actual timestamps.
+ LatinImeLogger.logOnAutoCorrectionForGeometric(
+ "", correctedWord, codePoint, wordComposer.getInputPointers());
+ }
+ }
+ }
+
+ public static void onAutoCorrectionCancellation() {
+ LatinImeLogger.logOnAutoCorrectionCancelled();
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/LocaleUtils.java b/java/src/com/android/inputmethod/latin/utils/LocaleUtils.java
new file mode 100644
index 000000000..22045aa38
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/LocaleUtils.java
@@ -0,0 +1,225 @@
+/*
+ * 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.utils;
+
+import android.text.TextUtils;
+
+import java.util.HashMap;
+import java.util.Locale;
+
+/**
+ * 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 in the dictionary pack. They need to be kept synchronized; for any
+ * update/bugfix to this file, consider also updating/fixing the version in the
+ * dictionary pack.
+ */
+public final class LocaleUtils {
+ private static final HashMap<String, Long> EMPTY_LT_HASH_MAP = CollectionUtils.newHashMap();
+ private static final String LOCALE_AND_TIME_STR_SEPARATER = ",";
+
+ 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(String referenceLocale, String testedLocale) {
+ if (TextUtils.isEmpty(referenceLocale)) {
+ return TextUtils.isEmpty(testedLocale) ? LOCALE_FULL_MATCH : LOCALE_ANY_MATCH;
+ }
+ if (null == testedLocale) return LOCALE_NO_MATCH;
+ String[] referenceParams = referenceLocale.split("_", 3);
+ 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(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(int level) {
+ return LOCALE_MATCH <= level;
+ }
+
+ private static final HashMap<String, Locale> sLocaleCache = CollectionUtils.newHashMap();
+
+ /**
+ * Creates a locale from a string specification.
+ */
+ public static Locale constructLocaleFromString(final String localeStr) {
+ if (localeStr == null)
+ return null;
+ synchronized (sLocaleCache) {
+ if (sLocaleCache.containsKey(localeStr))
+ return sLocaleCache.get(localeStr);
+ Locale retval = null;
+ String[] localeParams = localeStr.split("_", 3);
+ if (localeParams.length == 1) {
+ retval = new Locale(localeParams[0]);
+ } else if (localeParams.length == 2) {
+ retval = new Locale(localeParams[0], localeParams[1]);
+ } else if (localeParams.length == 3) {
+ retval = new Locale(localeParams[0], localeParams[1], localeParams[2]);
+ }
+ if (retval != null) {
+ sLocaleCache.put(localeStr, retval);
+ }
+ return retval;
+ }
+ }
+
+ public static HashMap<String, Long> localeAndTimeStrToHashMap(String str) {
+ if (TextUtils.isEmpty(str)) {
+ return EMPTY_LT_HASH_MAP;
+ }
+ final String[] ss = str.split(LOCALE_AND_TIME_STR_SEPARATER);
+ final int N = ss.length;
+ if (N < 2 || N % 2 != 0) {
+ return EMPTY_LT_HASH_MAP;
+ }
+ final HashMap<String, Long> retval = CollectionUtils.newHashMap();
+ for (int i = 0; i < N / 2; ++i) {
+ final String localeStr = ss[i * 2];
+ final long time = Long.valueOf(ss[i * 2 + 1]);
+ retval.put(localeStr, time);
+ }
+ return retval;
+ }
+
+ public static String localeAndTimeHashMapToStr(HashMap<String, Long> map) {
+ if (map == null || map.isEmpty()) {
+ return "";
+ }
+ final StringBuilder builder = new StringBuilder();
+ for (String localeStr : map.keySet()) {
+ if (builder.length() > 0) {
+ builder.append(LOCALE_AND_TIME_STR_SEPARATER);
+ }
+ final Long time = map.get(localeStr);
+ builder.append(localeStr).append(LOCALE_AND_TIME_STR_SEPARATER);
+ builder.append(String.valueOf(time));
+ }
+ return builder.toString();
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/MetadataFileUriGetter.java b/java/src/com/android/inputmethod/latin/utils/MetadataFileUriGetter.java
new file mode 100644
index 000000000..9ad319da6
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/MetadataFileUriGetter.java
@@ -0,0 +1,38 @@
+/*
+ * 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.utils;
+
+import com.android.inputmethod.latin.R;
+
+import android.content.Context;
+
+/**
+ * Helper class to get the metadata URI and the additional ID.
+ */
+public class MetadataFileUriGetter {
+ private MetadataFileUriGetter() {
+ // This helper class is not instantiable.
+ }
+
+ public static String getMetadataUri(final Context context) {
+ return context.getString(R.string.dictionary_pack_metadata_uri);
+ }
+
+ public static String getMetadataAdditionalId(final Context context) {
+ return "";
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/PrioritizedSerialExecutor.java b/java/src/com/android/inputmethod/latin/utils/PrioritizedSerialExecutor.java
new file mode 100644
index 000000000..5dc0b5893
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/PrioritizedSerialExecutor.java
@@ -0,0 +1,147 @@
+/*
+ * 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.utils;
+
+import java.util.ArrayDeque;
+import java.util.Queue;
+
+/**
+ * An object that executes submitted tasks using a thread.
+ */
+public class PrioritizedSerialExecutor {
+ public static final String TAG = PrioritizedSerialExecutor.class.getSimpleName();
+
+ private final Object mLock = new Object();
+
+ // The default value of capacities of task queues.
+ private static final int TASK_QUEUE_CAPACITY = 1000;
+ private final Queue<Runnable> mTasks;
+ private final Queue<Runnable> mPrioritizedTasks;
+ private boolean mIsShutdown;
+
+ // The task which is running now.
+ private Runnable mActive;
+
+ public PrioritizedSerialExecutor() {
+ mTasks = new ArrayDeque<Runnable>(TASK_QUEUE_CAPACITY);
+ mPrioritizedTasks = new ArrayDeque<Runnable>(TASK_QUEUE_CAPACITY);
+ mIsShutdown = false;
+ }
+
+ /**
+ * Clears all queued tasks.
+ */
+ public void clearAllTasks() {
+ synchronized(mLock) {
+ mTasks.clear();
+ mPrioritizedTasks.clear();
+ }
+ }
+
+ /**
+ * Enqueues the given task into the task queue.
+ * @param r the enqueued task
+ */
+ public void execute(final Runnable r) {
+ synchronized(mLock) {
+ if (!mIsShutdown) {
+ mTasks.offer(r);
+ if (mActive == null) {
+ scheduleNext();
+ }
+ }
+ }
+ }
+
+ /**
+ * Enqueues the given task into the prioritized task queue.
+ * @param r the enqueued task
+ */
+ public void executePrioritized(final Runnable r) {
+ synchronized(mLock) {
+ if (!mIsShutdown) {
+ mPrioritizedTasks.offer(r);
+ if (mActive == null) {
+ scheduleNext();
+ }
+ }
+ }
+ }
+
+ private boolean fetchNextTasks() {
+ synchronized(mLock) {
+ mActive = mPrioritizedTasks.poll();
+ if (mActive == null) {
+ mActive = mTasks.poll();
+ }
+ return mActive != null;
+ }
+ }
+
+ private void scheduleNext() {
+ synchronized(mLock) {
+ if (!fetchNextTasks()) {
+ return;
+ }
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ do {
+ synchronized(mLock) {
+ if (mActive != null) {
+ mActive.run();
+ }
+ }
+ } while (fetchNextTasks());
+ } finally {
+ scheduleNext();
+ }
+ }
+ }).start();
+ }
+ }
+
+ public void remove(final Runnable r) {
+ synchronized(mLock) {
+ mTasks.remove(r);
+ mPrioritizedTasks.remove(r);
+ }
+ }
+
+ public void replaceAndExecute(final Runnable oldTask, final Runnable newTask) {
+ synchronized(mLock) {
+ if (oldTask != null) remove(oldTask);
+ execute(newTask);
+ }
+ }
+
+ public void shutdown() {
+ synchronized(mLock) {
+ mIsShutdown = true;
+ }
+ }
+
+ public boolean isTerminated() {
+ synchronized(mLock) {
+ if (!mIsShutdown) {
+ return false;
+ }
+ return mPrioritizedTasks.isEmpty() && mTasks.isEmpty() && mActive == null;
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/RecapitalizeStatus.java b/java/src/com/android/inputmethod/latin/utils/RecapitalizeStatus.java
new file mode 100644
index 000000000..0f5cd80db
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/RecapitalizeStatus.java
@@ -0,0 +1,190 @@
+/*
+ * 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.utils;
+
+import java.util.Locale;
+
+/**
+ * The status of the current recapitalize process.
+ */
+public class RecapitalizeStatus {
+ public static final int NOT_A_RECAPITALIZE_MODE = -1;
+ public static final int CAPS_MODE_ORIGINAL_MIXED_CASE = 0;
+ public static final int CAPS_MODE_ALL_LOWER = 1;
+ public static final int CAPS_MODE_FIRST_WORD_UPPER = 2;
+ public static final int CAPS_MODE_ALL_UPPER = 3;
+ // When adding a new mode, don't forget to update the CAPS_MODE_LAST constant.
+ public static final int CAPS_MODE_LAST = CAPS_MODE_ALL_UPPER;
+
+ private static final int[] ROTATION_STYLE = {
+ CAPS_MODE_ORIGINAL_MIXED_CASE,
+ CAPS_MODE_ALL_LOWER,
+ CAPS_MODE_FIRST_WORD_UPPER,
+ CAPS_MODE_ALL_UPPER
+ };
+
+ private static final int getStringMode(final String string, final String separators) {
+ if (StringUtils.isIdenticalAfterUpcase(string)) {
+ return CAPS_MODE_ALL_UPPER;
+ } else if (StringUtils.isIdenticalAfterDowncase(string)) {
+ return CAPS_MODE_ALL_LOWER;
+ } else if (StringUtils.isIdenticalAfterCapitalizeEachWord(string, separators)) {
+ return CAPS_MODE_FIRST_WORD_UPPER;
+ } else {
+ return CAPS_MODE_ORIGINAL_MIXED_CASE;
+ }
+ }
+
+ /**
+ * We store the location of the cursor and the string that was there before the recapitalize
+ * action was done, and the location of the cursor and the string that was there after.
+ */
+ private int mCursorStartBefore;
+ private String mStringBefore;
+ private int mCursorStartAfter;
+ private int mCursorEndAfter;
+ private int mRotationStyleCurrentIndex;
+ private boolean mSkipOriginalMixedCaseMode;
+ private Locale mLocale;
+ private String mSeparators;
+ private String mStringAfter;
+ private boolean mIsActive;
+
+ public RecapitalizeStatus() {
+ // By default, initialize with dummy values that won't match any real recapitalize.
+ initialize(-1, -1, "", Locale.getDefault(), "");
+ deactivate();
+ }
+
+ public void initialize(final int cursorStart, final int cursorEnd, final String string,
+ final Locale locale, final String separators) {
+ mCursorStartBefore = cursorStart;
+ mStringBefore = string;
+ mCursorStartAfter = cursorStart;
+ mCursorEndAfter = cursorEnd;
+ mStringAfter = string;
+ final int initialMode = getStringMode(mStringBefore, separators);
+ mLocale = locale;
+ mSeparators = separators;
+ if (CAPS_MODE_ORIGINAL_MIXED_CASE == initialMode) {
+ mRotationStyleCurrentIndex = 0;
+ mSkipOriginalMixedCaseMode = false;
+ } else {
+ // Find the current mode in the array.
+ int currentMode;
+ for (currentMode = ROTATION_STYLE.length - 1; currentMode > 0; --currentMode) {
+ if (ROTATION_STYLE[currentMode] == initialMode) {
+ break;
+ }
+ }
+ mRotationStyleCurrentIndex = currentMode;
+ mSkipOriginalMixedCaseMode = true;
+ }
+ mIsActive = true;
+ }
+
+ public void deactivate() {
+ mIsActive = false;
+ }
+
+ public boolean isActive() {
+ return mIsActive;
+ }
+
+ public boolean isSetAt(final int cursorStart, final int cursorEnd) {
+ return cursorStart == mCursorStartAfter && cursorEnd == mCursorEndAfter;
+ }
+
+ /**
+ * Rotate through the different possible capitalization modes.
+ */
+ public void rotate() {
+ final String oldResult = mStringAfter;
+ int count = 0; // Protection against infinite loop.
+ do {
+ mRotationStyleCurrentIndex = (mRotationStyleCurrentIndex + 1) % ROTATION_STYLE.length;
+ if (CAPS_MODE_ORIGINAL_MIXED_CASE == ROTATION_STYLE[mRotationStyleCurrentIndex]
+ && mSkipOriginalMixedCaseMode) {
+ mRotationStyleCurrentIndex =
+ (mRotationStyleCurrentIndex + 1) % ROTATION_STYLE.length;
+ }
+ ++count;
+ switch (ROTATION_STYLE[mRotationStyleCurrentIndex]) {
+ case CAPS_MODE_ORIGINAL_MIXED_CASE:
+ mStringAfter = mStringBefore;
+ break;
+ case CAPS_MODE_ALL_LOWER:
+ mStringAfter = mStringBefore.toLowerCase(mLocale);
+ break;
+ case CAPS_MODE_FIRST_WORD_UPPER:
+ mStringAfter = StringUtils.capitalizeEachWord(mStringBefore, mSeparators,
+ mLocale);
+ break;
+ case CAPS_MODE_ALL_UPPER:
+ mStringAfter = mStringBefore.toUpperCase(mLocale);
+ break;
+ default:
+ mStringAfter = mStringBefore;
+ }
+ } while (mStringAfter.equals(oldResult) && count < ROTATION_STYLE.length + 1);
+ mCursorEndAfter = mCursorStartAfter + mStringAfter.length();
+ }
+
+ /**
+ * Remove leading/trailing whitespace from the considered string.
+ */
+ public void trim() {
+ final int len = mStringBefore.length();
+ int nonWhitespaceStart = 0;
+ for (; nonWhitespaceStart < len;
+ nonWhitespaceStart = mStringBefore.offsetByCodePoints(nonWhitespaceStart, 1)) {
+ final int codePoint = mStringBefore.codePointAt(nonWhitespaceStart);
+ if (!Character.isWhitespace(codePoint)) break;
+ }
+ int nonWhitespaceEnd = len;
+ for (; nonWhitespaceEnd > 0;
+ nonWhitespaceEnd = mStringBefore.offsetByCodePoints(nonWhitespaceEnd, -1)) {
+ final int codePoint = mStringBefore.codePointBefore(nonWhitespaceEnd);
+ if (!Character.isWhitespace(codePoint)) break;
+ }
+ // If nonWhitespaceStart >= nonWhitespaceEnd, that means the selection contained only
+ // whitespace, so we leave it as is.
+ if ((0 != nonWhitespaceStart || len != nonWhitespaceEnd)
+ && nonWhitespaceStart < nonWhitespaceEnd) {
+ mCursorEndAfter = mCursorStartBefore + nonWhitespaceEnd;
+ mCursorStartBefore = mCursorStartAfter = mCursorStartBefore + nonWhitespaceStart;
+ mStringAfter = mStringBefore =
+ mStringBefore.substring(nonWhitespaceStart, nonWhitespaceEnd);
+ }
+ }
+
+ public String getRecapitalizedString() {
+ return mStringAfter;
+ }
+
+ public int getNewCursorStart() {
+ return mCursorStartAfter;
+ }
+
+ public int getNewCursorEnd() {
+ return mCursorEndAfter;
+ }
+
+ public int getCurrentMode() {
+ return ROTATION_STYLE[mRotationStyleCurrentIndex];
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/ResizableIntArray.java b/java/src/com/android/inputmethod/latin/utils/ResizableIntArray.java
new file mode 100644
index 000000000..7c6fe93ac
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/ResizableIntArray.java
@@ -0,0 +1,155 @@
+/*
+ * 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.utils;
+
+import java.util.Arrays;
+
+// TODO: This class is not thread-safe.
+public final class ResizableIntArray {
+ 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 add(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;
+ }
+
+ public int[] getPrimitiveArray() {
+ return mArray;
+ }
+
+ public void set(final ResizableIntArray ip) {
+ // TODO: Implement primitive array pool.
+ mArray = ip.mArray;
+ mLength = ip.mLength;
+ }
+
+ public void copy(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(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/java/src/com/android/inputmethod/latin/utils/ResourceUtils.java b/java/src/com/android/inputmethod/latin/utils/ResourceUtils.java
new file mode 100644
index 000000000..22c92446a
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/ResourceUtils.java
@@ -0,0 +1,323 @@
+/*
+ * 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.utils;
+
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.os.Build;
+import android.text.TextUtils;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.TypedValue;
+
+import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.latin.R;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.regex.PatternSyntaxException;
+
+public final class ResourceUtils {
+ private static final String TAG = ResourceUtils.class.getSimpleName();
+
+ public static final float UNDEFINED_RATIO = -1.0f;
+ public static final int UNDEFINED_DIMENSION = -1;
+
+ private ResourceUtils() {
+ // This utility class is not publicly instantiable.
+ }
+
+ private static final HashMap<String, String> sDeviceOverrideValueMap =
+ CollectionUtils.newHashMap();
+
+ private static final String[] BUILD_KEYS_AND_VALUES = {
+ "HARDWARE", Build.HARDWARE,
+ "MODEL", Build.MODEL,
+ "BRAND", Build.BRAND,
+ "MANUFACTURER", Build.MANUFACTURER
+ };
+ private static final HashMap<String, String> sBuildKeyValues;
+ private static final String sBuildKeyValuesDebugString;
+
+ static {
+ sBuildKeyValues = CollectionUtils.newHashMap();
+ final ArrayList<String> keyValuePairs = CollectionUtils.newArrayList();
+ final int keyCount = BUILD_KEYS_AND_VALUES.length / 2;
+ for (int i = 0; i < keyCount; i++) {
+ final int index = i * 2;
+ final String key = BUILD_KEYS_AND_VALUES[index];
+ final String value = BUILD_KEYS_AND_VALUES[index + 1];
+ sBuildKeyValues.put(key, value);
+ keyValuePairs.add(key + '=' + value);
+ }
+ sBuildKeyValuesDebugString = "[" + TextUtils.join(" ", keyValuePairs) + "]";
+ }
+
+ public static String getDeviceOverrideValue(final Resources res, final int overrideResId) {
+ final int orientation = res.getConfiguration().orientation;
+ final String key = overrideResId + "-" + orientation;
+ if (sDeviceOverrideValueMap.containsKey(key)) {
+ return sDeviceOverrideValueMap.get(key);
+ }
+
+ final String[] overrideArray = res.getStringArray(overrideResId);
+ final String overrideValue = findConstantForKeyValuePairs(sBuildKeyValues, overrideArray);
+ // The overrideValue might be an empty string.
+ if (overrideValue != null) {
+ Log.i(TAG, "Find override value:"
+ + " resource="+ res.getResourceEntryName(overrideResId)
+ + " build=" + sBuildKeyValuesDebugString
+ + " override=" + overrideValue);
+ sDeviceOverrideValueMap.put(key, overrideValue);
+ return overrideValue;
+ }
+
+ String defaultValue = null;
+ try {
+ defaultValue = findDefaultConstant(overrideArray);
+ // The defaultValue might be an empty string.
+ if (defaultValue == null) {
+ Log.w(TAG, "Couldn't find override value nor default value:"
+ + " resource="+ res.getResourceEntryName(overrideResId)
+ + " build=" + sBuildKeyValuesDebugString);
+ } else {
+ Log.i(TAG, "Found default value:"
+ + " resource="+ res.getResourceEntryName(overrideResId)
+ + " build=" + sBuildKeyValuesDebugString
+ + " default=" + defaultValue);
+ }
+ } catch (final DeviceOverridePatternSyntaxError e) {
+ Log.w(TAG, "Syntax error, ignored", e);
+ }
+ sDeviceOverrideValueMap.put(key, defaultValue);
+ return defaultValue;
+ }
+
+ @SuppressWarnings("serial")
+ static class DeviceOverridePatternSyntaxError extends Exception {
+ public DeviceOverridePatternSyntaxError(final String message, final String expression) {
+ this(message, expression, null);
+ }
+
+ public DeviceOverridePatternSyntaxError(final String message, final String expression,
+ final Throwable throwable) {
+ super(message + ": " + expression, throwable);
+ }
+ }
+
+ /**
+ * Find the condition that fulfills specified key value pairs from an array of
+ * "condition,constant", and return the corresponding string constant. A condition is
+ * "pattern1[:pattern2...] (or an empty string for the default). A pattern is
+ * "key=regexp_value" string. The condition matches only if all patterns of the condition
+ * are true for the specified key value pairs.
+ *
+ * For example, "condition,constant" has the following format.
+ * (See {@link ResourceUtilsTests#testFindConstantForKeyValuePairsRegexp()})
+ * - HARDWARE=mako,constantForNexus4
+ * - MODEL=Nexus 4:MANUFACTURER=LGE,constantForNexus4
+ * - ,defaultConstant
+ *
+ * @param keyValuePairs attributes to be used to look for a matched condition.
+ * @param conditionConstantArray an array of "condition,constant" elements to be searched.
+ * @return the constant part of the matched "condition,constant" element. Returns null if no
+ * condition matches.
+ */
+ @UsedForTesting
+ static String findConstantForKeyValuePairs(final HashMap<String, String> keyValuePairs,
+ final String[] conditionConstantArray) {
+ if (conditionConstantArray == null || keyValuePairs == null) {
+ return null;
+ }
+ String foundValue = null;
+ for (final String conditionConstant : conditionConstantArray) {
+ final int posComma = conditionConstant.indexOf(',');
+ if (posComma < 0) {
+ Log.w(TAG, "Array element has no comma: " + conditionConstant);
+ continue;
+ }
+ final String condition = conditionConstant.substring(0, posComma);
+ if (condition.isEmpty()) {
+ // Default condition. The default condition should be searched by
+ // {@link #findConstantForDefault(String[])}.
+ continue;
+ }
+ try {
+ if (fulfillsCondition(keyValuePairs, condition)) {
+ // Take first match
+ if (foundValue == null) {
+ foundValue = conditionConstant.substring(posComma + 1);
+ }
+ // And continue walking through all conditions.
+ }
+ } catch (final DeviceOverridePatternSyntaxError e) {
+ Log.w(TAG, "Syntax error, ignored", e);
+ }
+ }
+ return foundValue;
+ }
+
+ private static boolean fulfillsCondition(final HashMap<String,String> keyValuePairs,
+ final String condition) throws DeviceOverridePatternSyntaxError {
+ final String[] patterns = condition.split(":");
+ // Check all patterns in a condition are true
+ boolean matchedAll = true;
+ for (final String pattern : patterns) {
+ final int posEqual = pattern.indexOf('=');
+ if (posEqual < 0) {
+ throw new DeviceOverridePatternSyntaxError("Pattern has no '='", condition);
+ }
+ final String key = pattern.substring(0, posEqual);
+ final String value = keyValuePairs.get(key);
+ if (value == null) {
+ throw new DeviceOverridePatternSyntaxError("Unknown key", condition);
+ }
+ final String patternRegexpValue = pattern.substring(posEqual + 1);
+ try {
+ if (!value.matches(patternRegexpValue)) {
+ matchedAll = false;
+ // And continue walking through all patterns.
+ }
+ } catch (final PatternSyntaxException e) {
+ throw new DeviceOverridePatternSyntaxError("Syntax error", condition, e);
+ }
+ }
+ return matchedAll;
+ }
+
+ @UsedForTesting
+ static String findDefaultConstant(final String[] conditionConstantArray)
+ throws DeviceOverridePatternSyntaxError {
+ if (conditionConstantArray == null) {
+ return null;
+ }
+ for (final String condition : conditionConstantArray) {
+ final int posComma = condition.indexOf(',');
+ if (posComma < 0) {
+ throw new DeviceOverridePatternSyntaxError("Array element has no comma", condition);
+ }
+ if (posComma == 0) { // condition is empty.
+ return condition.substring(posComma + 1);
+ }
+ }
+ return null;
+ }
+
+ public static int getDefaultKeyboardWidth(final Resources res) {
+ final DisplayMetrics dm = res.getDisplayMetrics();
+ return dm.widthPixels;
+ }
+
+ public static int getDefaultKeyboardHeight(final Resources res) {
+ final DisplayMetrics dm = res.getDisplayMetrics();
+ final String keyboardHeightString = getDeviceOverrideValue(res, R.array.keyboard_heights);
+ final float keyboardHeight;
+ if (TextUtils.isEmpty(keyboardHeightString)) {
+ keyboardHeight = res.getDimension(R.dimen.keyboardHeight);
+ } else {
+ keyboardHeight = Float.parseFloat(keyboardHeightString) * dm.density;
+ }
+ final float maxKeyboardHeight = res.getFraction(
+ R.fraction.maxKeyboardHeight, dm.heightPixels, dm.heightPixels);
+ float minKeyboardHeight = res.getFraction(
+ R.fraction.minKeyboardHeight, dm.heightPixels, dm.heightPixels);
+ if (minKeyboardHeight < 0.0f) {
+ // Specified fraction was negative, so it should be calculated against display
+ // width.
+ minKeyboardHeight = -res.getFraction(
+ R.fraction.minKeyboardHeight, dm.widthPixels, dm.widthPixels);
+ }
+ // Keyboard height will not exceed maxKeyboardHeight and will not be less than
+ // minKeyboardHeight.
+ return (int)Math.max(Math.min(keyboardHeight, maxKeyboardHeight), minKeyboardHeight);
+ }
+
+ public static boolean isValidFraction(final float fraction) {
+ return fraction >= 0.0f;
+ }
+
+ // {@link Resources#getDimensionPixelSize(int)} returns at least one pixel size.
+ public static boolean isValidDimensionPixelSize(final int dimension) {
+ return dimension > 0;
+ }
+
+ // {@link Resources#getDimensionPixelOffset(int)} may return zero pixel offset.
+ public static boolean isValidDimensionPixelOffset(final int dimension) {
+ return dimension >= 0;
+ }
+
+ public static float getFraction(final TypedArray a, final int index, final float defValue) {
+ final TypedValue value = a.peekValue(index);
+ if (value == null || !isFractionValue(value)) {
+ return defValue;
+ }
+ return a.getFraction(index, 1, 1, defValue);
+ }
+
+ public static float getFraction(final TypedArray a, final int index) {
+ return getFraction(a, index, UNDEFINED_RATIO);
+ }
+
+ public static int getDimensionPixelSize(final TypedArray a, final int index) {
+ final TypedValue value = a.peekValue(index);
+ if (value == null || !isDimensionValue(value)) {
+ return ResourceUtils.UNDEFINED_DIMENSION;
+ }
+ return a.getDimensionPixelSize(index, ResourceUtils.UNDEFINED_DIMENSION);
+ }
+
+ public static float getDimensionOrFraction(final TypedArray a, final int index, final int base,
+ final float defValue) {
+ final TypedValue value = a.peekValue(index);
+ if (value == null) {
+ return defValue;
+ }
+ if (isFractionValue(value)) {
+ return a.getFraction(index, base, base, defValue);
+ } else if (isDimensionValue(value)) {
+ return a.getDimension(index, defValue);
+ }
+ return defValue;
+ }
+
+ public static int getEnumValue(final TypedArray a, final int index, final int defValue) {
+ final TypedValue value = a.peekValue(index);
+ if (value == null) {
+ return defValue;
+ }
+ if (isIntegerValue(value)) {
+ return a.getInt(index, defValue);
+ }
+ return defValue;
+ }
+
+ public static boolean isFractionValue(final TypedValue v) {
+ return v.type == TypedValue.TYPE_FRACTION;
+ }
+
+ public static boolean isDimensionValue(final TypedValue v) {
+ return v.type == TypedValue.TYPE_DIMENSION;
+ }
+
+ public static boolean isIntegerValue(final TypedValue v) {
+ return v.type >= TypedValue.TYPE_FIRST_INT && v.type <= TypedValue.TYPE_LAST_INT;
+ }
+
+ public static boolean isStringValue(final TypedValue v) {
+ return v.type == TypedValue.TYPE_STRING;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/RunInLocale.java b/java/src/com/android/inputmethod/latin/utils/RunInLocale.java
new file mode 100644
index 000000000..2c9e3b191
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/RunInLocale.java
@@ -0,0 +1,55 @@
+/*
+ * 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.utils;
+
+import android.content.res.Configuration;
+import android.content.res.Resources;
+
+import java.util.Locale;
+
+public abstract class RunInLocale<T> {
+ private static final Object sLockForRunInLocale = new Object();
+
+ protected abstract T job(final Resources res);
+
+ /**
+ * Execute {@link #job(Resources)} method in specified system locale exclusively.
+ *
+ * @param res the resources to use.
+ * @param newLocale the locale to change to.
+ * @return the value returned from {@link #job(Resources)}.
+ */
+ public T runInLocale(final Resources res, final Locale newLocale) {
+ synchronized (sLockForRunInLocale) {
+ final Configuration conf = res.getConfiguration();
+ final Locale oldLocale = conf.locale;
+ final boolean needsChange = (newLocale != null && !newLocale.equals(oldLocale));
+ try {
+ if (needsChange) {
+ conf.locale = newLocale;
+ res.updateConfiguration(conf, null);
+ }
+ return job(res);
+ } finally {
+ if (needsChange) {
+ conf.locale = oldLocale;
+ res.updateConfiguration(conf, null);
+ }
+ }
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/SpannableStringUtils.java b/java/src/com/android/inputmethod/latin/utils/SpannableStringUtils.java
new file mode 100644
index 000000000..b51fd9377
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/SpannableStringUtils.java
@@ -0,0 +1,110 @@
+/*
+ * 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.utils;
+
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.text.SpannedString;
+import android.text.TextUtils;
+import android.text.style.SuggestionSpan;
+
+public final class SpannableStringUtils {
+ /**
+ * Copies the spans from the region <code>start...end</code> in
+ * <code>source</code> to the region
+ * <code>destoff...destoff+end-start</code> in <code>dest</code>.
+ * Spans in <code>source</code> that begin before <code>start</code>
+ * or end after <code>end</code> but overlap this range are trimmed
+ * as if they began at <code>start</code> or ended at <code>end</code>.
+ * Only SuggestionSpans that don't have the SPAN_PARAGRAPH span are copied.
+ *
+ * This code is almost entirely taken from {@link TextUtils#copySpansFrom}, except for the
+ * kind of span that is copied.
+ *
+ * @throws IndexOutOfBoundsException if any of the copied spans
+ * are out of range in <code>dest</code>.
+ */
+ public static void copyNonParagraphSuggestionSpansFrom(Spanned source, int start, int end,
+ Spannable dest, int destoff) {
+ Object[] spans = source.getSpans(start, end, SuggestionSpan.class);
+
+ for (int i = 0; i < spans.length; i++) {
+ int fl = source.getSpanFlags(spans[i]);
+ if (0 != (fl & Spannable.SPAN_PARAGRAPH)) continue;
+
+ int st = source.getSpanStart(spans[i]);
+ int en = source.getSpanEnd(spans[i]);
+
+ if (st < start)
+ st = start;
+ if (en > end)
+ en = end;
+
+ dest.setSpan(spans[i], st - start + destoff, en - start + destoff,
+ fl);
+ }
+ }
+
+ /**
+ * Returns a CharSequence concatenating the specified CharSequences, retaining their
+ * SuggestionSpans that don't have the PARAGRAPH flag, but not other spans.
+ *
+ * This code is almost entirely taken from {@link TextUtils#concat(CharSequence...)}, except
+ * it calls copyNonParagraphSuggestionSpansFrom instead of {@link TextUtils#copySpansFrom}.
+ */
+ public static CharSequence concatWithNonParagraphSuggestionSpansOnly(CharSequence... text) {
+ if (text.length == 0) {
+ return "";
+ }
+
+ if (text.length == 1) {
+ return text[0];
+ }
+
+ boolean spanned = false;
+ for (int i = 0; i < text.length; i++) {
+ if (text[i] instanceof Spanned) {
+ spanned = true;
+ break;
+ }
+ }
+
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < text.length; i++) {
+ sb.append(text[i]);
+ }
+
+ if (!spanned) {
+ return sb.toString();
+ }
+
+ SpannableString ss = new SpannableString(sb);
+ int off = 0;
+ for (int i = 0; i < text.length; i++) {
+ int len = text[i].length();
+
+ if (text[i] instanceof Spanned) {
+ copyNonParagraphSuggestionSpansFrom((Spanned) text[i], 0, len, ss, off);
+ }
+
+ off += len;
+ }
+
+ return new SpannedString(ss);
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/StaticInnerHandlerWrapper.java b/java/src/com/android/inputmethod/latin/utils/StaticInnerHandlerWrapper.java
new file mode 100644
index 000000000..44e5d17b4
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/StaticInnerHandlerWrapper.java
@@ -0,0 +1,42 @@
+/*
+ * 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.utils;
+
+import android.os.Handler;
+import android.os.Looper;
+
+import java.lang.ref.WeakReference;
+
+public class StaticInnerHandlerWrapper<T> extends Handler {
+ private final WeakReference<T> mOuterInstanceRef;
+
+ public StaticInnerHandlerWrapper(final T outerInstance) {
+ this(outerInstance, Looper.myLooper());
+ }
+
+ public StaticInnerHandlerWrapper(final T outerInstance, final Looper looper) {
+ super(looper);
+ if (outerInstance == null) {
+ throw new NullPointerException("outerInstance is null");
+ }
+ mOuterInstanceRef = new WeakReference<T>(outerInstance);
+ }
+
+ public T getOuterInstance() {
+ return mOuterInstanceRef.get();
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/StringUtils.java b/java/src/com/android/inputmethod/latin/utils/StringUtils.java
new file mode 100644
index 000000000..121aecf0f
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/StringUtils.java
@@ -0,0 +1,465 @@
+/*
+ * 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.utils;
+
+import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.settings.SettingsValues;
+
+import android.text.TextUtils;
+import android.util.JsonReader;
+import android.util.JsonWriter;
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+
+public final class StringUtils {
+ private static final String TAG = StringUtils.class.getSimpleName();
+ 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
+
+ private StringUtils() {
+ // This utility class is not publicly instantiable.
+ }
+
+ public static int codePointCount(final String text) {
+ if (TextUtils.isEmpty(text)) return 0;
+ return text.codePointCount(0, text.length());
+ }
+
+ public static boolean containsInArray(final String text, 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.
+ */
+ private static final String SEPARATOR_FOR_COMMA_SPLITTABLE_TEXT = ",";
+
+ public static boolean containsInCommaSplittableText(final String text,
+ final String extraValues) {
+ if (TextUtils.isEmpty(extraValues)) {
+ return false;
+ }
+ return containsInArray(text, extraValues.split(SEPARATOR_FOR_COMMA_SPLITTABLE_TEXT));
+ }
+
+ public static String appendToCommaSplittableTextIfNotExists(final String text,
+ final String extraValues) {
+ if (TextUtils.isEmpty(extraValues)) {
+ return text;
+ }
+ if (containsInCommaSplittableText(text, extraValues)) {
+ return extraValues;
+ }
+ return extraValues + SEPARATOR_FOR_COMMA_SPLITTABLE_TEXT + text;
+ }
+
+ public static String removeFromCommaSplittableTextIfExists(final String text,
+ final String extraValues) {
+ if (TextUtils.isEmpty(extraValues)) {
+ return "";
+ }
+ final String[] elements = extraValues.split(SEPARATOR_FOR_COMMA_SPLITTABLE_TEXT);
+ if (!containsInArray(text, elements)) {
+ return extraValues;
+ }
+ final ArrayList<String> result = CollectionUtils.newArrayList(elements.length - 1);
+ for (final String element : elements) {
+ if (!text.equals(element)) {
+ result.add(element);
+ }
+ }
+ return TextUtils.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(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 (TextUtils.equals(cur, previous)) {
+ suggestions.remove(i);
+ i--;
+ break;
+ }
+ }
+ i++;
+ }
+ }
+
+ public static String capitalizeFirstCodePoint(final String s, final Locale locale) {
+ if (s.length() <= 1) {
+ return s.toUpperCase(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(locale) + s.substring(cutoff);
+ }
+
+ public static String capitalizeFirstAndDowncaseRest(final String s, final Locale locale) {
+ if (s.length() <= 1) {
+ return s.toUpperCase(locale);
+ }
+ // TODO: fix the bugs below
+ // - This does not work for Greek, because it returns upper case instead of title case.
+ // - 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(locale) + s.substring(cutoff).toLowerCase(locale);
+ }
+
+ private static final int[] EMPTY_CODEPOINTS = {};
+
+ public static int[] toCodePointArray(final String string) {
+ final int length = string.length();
+ if (length <= 0) {
+ return EMPTY_CODEPOINTS;
+ }
+ final int[] codePoints = new int[string.codePointCount(0, length)];
+ int destIndex = 0;
+ for (int index = 0; index < length; index = string.offsetByCodePoints(index, 1)) {
+ codePoints[destIndex] = string.codePointAt(index);
+ destIndex++;
+ }
+ return codePoints;
+ }
+
+ // This method assumes the text is not null. For the empty string, it returns CAPITALIZE_NONE.
+ public static int getCapitalizationType(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(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(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;
+ }
+
+ @UsedForTesting
+ public static boolean looksValidForDictionaryInsertion(final CharSequence text,
+ final SettingsValues settings) {
+ if (TextUtils.isEmpty(text)) return false;
+ final int length = text.length();
+ int i = 0;
+ int digitCount = 0;
+ while (i < length) {
+ final int codePoint = Character.codePointAt(text, i);
+ final int charCount = Character.charCount(codePoint);
+ i += charCount;
+ if (Character.isDigit(codePoint)) {
+ // Count digits: see below
+ digitCount += charCount;
+ continue;
+ }
+ if (!settings.isWordCodePoint(codePoint)) return false;
+ }
+ // We reject strings entirely comprised of digits to avoid using PIN codes or credit
+ // card numbers. It would come in handy for word prediction though; a good example is
+ // when writing one's address where the street number is usually quite discriminative,
+ // as well as the postal code.
+ return digitCount < length;
+ }
+
+ public static boolean isIdenticalAfterCapitalizeEachWord(final String text,
+ final String separators) {
+ boolean needCapsNext = 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 ((needCapsNext && !Character.isUpperCase(codePoint))
+ || (!needCapsNext && !Character.isLowerCase(codePoint))) {
+ return false;
+ }
+ }
+ // We need a capital letter next if this is a separator.
+ needCapsNext = (-1 != separators.indexOf(codePoint));
+ }
+ 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.
+ public static String capitalizeEachWord(final String text, final String separators,
+ final Locale locale) {
+ final StringBuilder builder = new StringBuilder();
+ boolean needCapsNext = 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 (needCapsNext) {
+ builder.append(nextChar.toUpperCase(locale));
+ } else {
+ builder.append(nextChar.toLowerCase(locale));
+ }
+ // We need a capital letter next if this is a separator.
+ needCapsNext = (-1 != separators.indexOf(nextChar.codePointAt(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(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;
+ }
+
+ public static boolean isEmptyStringOrWhiteSpaces(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
+ public static String byteArrayToHexString(byte[] bytes) {
+ if (bytes == null || bytes.length == 0) {
+ return "";
+ }
+ final StringBuilder sb = new StringBuilder();
+ for (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
+ public static byte[] hexStringToByteArray(String hexString) {
+ if (TextUtils.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;
+ }
+
+ public static List<Object> jsonStrToList(String s) {
+ final ArrayList<Object> retval = CollectionUtils.newArrayList();
+ final JsonReader reader = new JsonReader(new StringReader(s));
+ try {
+ reader.beginArray();
+ while(reader.hasNext()) {
+ reader.beginObject();
+ while (reader.hasNext()) {
+ final String name = reader.nextName();
+ if (name.equals(Integer.class.getSimpleName())) {
+ retval.add(reader.nextInt());
+ } else if (name.equals(String.class.getSimpleName())) {
+ retval.add(reader.nextString());
+ } else {
+ Log.w(TAG, "Invalid name: " + name);
+ reader.skipValue();
+ }
+ }
+ reader.endObject();
+ }
+ reader.endArray();
+ return retval;
+ } catch (IOException e) {
+ } finally {
+ try {
+ reader.close();
+ } catch (IOException e) {
+ }
+ }
+ return Collections.<Object>emptyList();
+ }
+
+ public static String listToJsonStr(List<Object> list) {
+ if (list == null || list.isEmpty()) {
+ return "";
+ }
+ final StringWriter sw = new StringWriter();
+ final JsonWriter writer = new JsonWriter(sw);
+ try {
+ writer.beginArray();
+ for (final Object o : list) {
+ writer.beginObject();
+ if (o instanceof Integer) {
+ writer.name(Integer.class.getSimpleName()).value((Integer)o);
+ } else if (o instanceof String) {
+ writer.name(String.class.getSimpleName()).value((String)o);
+ }
+ writer.endObject();
+ }
+ writer.endArray();
+ return sw.toString();
+ } catch (IOException e) {
+ } finally {
+ try {
+ if (writer != null) {
+ writer.close();
+ }
+ } catch (IOException e) {
+ }
+ }
+ return "";
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java b/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java
new file mode 100644
index 000000000..102a41b4e
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java
@@ -0,0 +1,333 @@
+/*
+ * 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.utils;
+
+import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET;
+import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.Build;
+import android.util.Log;
+import android.view.inputmethod.InputMethodSubtype;
+
+import com.android.inputmethod.latin.DictionaryFactory;
+import com.android.inputmethod.latin.R;
+
+import java.util.HashMap;
+import java.util.Locale;
+
+public final class SubtypeLocaleUtils {
+ static final String TAG = SubtypeLocaleUtils.class.getSimpleName();
+ // This class must be located in the same package as LatinIME.java.
+ private static final String RESOURCE_PACKAGE_NAME =
+ DictionaryFactory.class.getPackage().getName();
+
+ // Special language code to represent "no language".
+ public static final String NO_LANGUAGE = "zz";
+ public static final String QWERTY = "qwerty";
+ public static final String EMOJI = "emoji";
+ public static final int UNKNOWN_KEYBOARD_LAYOUT = R.string.subtype_generic;
+
+ private static boolean sInitialized = false;
+ private static Resources sResources;
+ private static String[] sPredefinedKeyboardLayoutSet;
+ // Keyboard layout to its display name map.
+ private static final HashMap<String, String> sKeyboardLayoutToDisplayNameMap =
+ CollectionUtils.newHashMap();
+ // Keyboard layout to subtype name resource id map.
+ private static final HashMap<String, Integer> sKeyboardLayoutToNameIdsMap =
+ CollectionUtils.newHashMap();
+ // Exceptional locale to subtype name resource id map.
+ private static final HashMap<String, Integer> sExceptionalLocaleToNameIdsMap =
+ CollectionUtils.newHashMap();
+ // Exceptional locale to subtype name with layout resource id map.
+ private static final HashMap<String, Integer> sExceptionalLocaleToWithLayoutNameIdsMap =
+ CollectionUtils.newHashMap();
+ private static final String SUBTYPE_NAME_RESOURCE_PREFIX =
+ "string/subtype_";
+ private static final String SUBTYPE_NAME_RESOURCE_GENERIC_PREFIX =
+ "string/subtype_generic_";
+ private static final String SUBTYPE_NAME_RESOURCE_WITH_LAYOUT_PREFIX =
+ "string/subtype_with_layout_";
+ private static final String SUBTYPE_NAME_RESOURCE_NO_LANGUAGE_PREFIX =
+ "string/subtype_no_language_";
+ // Keyboard layout set name for the subtypes that don't have a keyboardLayoutSet extra value.
+ // This is for compatibility to keep the same subtype ids as pre-JellyBean.
+ private static final HashMap<String, String> sLocaleAndExtraValueToKeyboardLayoutSetMap =
+ CollectionUtils.newHashMap();
+
+ private SubtypeLocaleUtils() {
+ // Intentional empty constructor for utility class.
+ }
+
+ // Note that this initialization method can be called multiple times.
+ public static synchronized void init(final Context context) {
+ if (sInitialized) return;
+
+ final Resources res = context.getResources();
+ sResources = res;
+
+ final String[] predefinedLayoutSet = res.getStringArray(R.array.predefined_layouts);
+ sPredefinedKeyboardLayoutSet = predefinedLayoutSet;
+ final String[] layoutDisplayNames = res.getStringArray(
+ R.array.predefined_layout_display_names);
+ for (int i = 0; i < predefinedLayoutSet.length; i++) {
+ final String layoutName = predefinedLayoutSet[i];
+ sKeyboardLayoutToDisplayNameMap.put(layoutName, layoutDisplayNames[i]);
+ final String resourceName = SUBTYPE_NAME_RESOURCE_GENERIC_PREFIX + layoutName;
+ final int resId = res.getIdentifier(resourceName, null, RESOURCE_PACKAGE_NAME);
+ sKeyboardLayoutToNameIdsMap.put(layoutName, resId);
+ // Register subtype name resource id of "No language" with key "zz_<layout>"
+ final String noLanguageResName = SUBTYPE_NAME_RESOURCE_NO_LANGUAGE_PREFIX + layoutName;
+ final int noLanguageResId = res.getIdentifier(
+ noLanguageResName, null, RESOURCE_PACKAGE_NAME);
+ final String key = getNoLanguageLayoutKey(layoutName);
+ sKeyboardLayoutToNameIdsMap.put(key, noLanguageResId);
+ }
+
+ final String[] exceptionalLocales = res.getStringArray(
+ R.array.subtype_locale_exception_keys);
+ for (int i = 0; i < exceptionalLocales.length; i++) {
+ final String localeString = exceptionalLocales[i];
+ final String resourceName = SUBTYPE_NAME_RESOURCE_PREFIX + localeString;
+ final int resId = res.getIdentifier(resourceName, null, RESOURCE_PACKAGE_NAME);
+ sExceptionalLocaleToNameIdsMap.put(localeString, resId);
+ final String resourceNameWithLayout =
+ SUBTYPE_NAME_RESOURCE_WITH_LAYOUT_PREFIX + localeString;
+ final int resIdWithLayout = res.getIdentifier(
+ resourceNameWithLayout, null, RESOURCE_PACKAGE_NAME);
+ sExceptionalLocaleToWithLayoutNameIdsMap.put(localeString, resIdWithLayout);
+ }
+
+ final String[] keyboardLayoutSetMap = res.getStringArray(
+ R.array.locale_and_extra_value_to_keyboard_layout_set_map);
+ for (int i = 0; i + 1 < keyboardLayoutSetMap.length; i += 2) {
+ final String key = keyboardLayoutSetMap[i];
+ final String keyboardLayoutSet = keyboardLayoutSetMap[i + 1];
+ sLocaleAndExtraValueToKeyboardLayoutSetMap.put(key, keyboardLayoutSet);
+ }
+
+ sInitialized = true;
+ }
+
+ public static String[] getPredefinedKeyboardLayoutSet() {
+ return sPredefinedKeyboardLayoutSet;
+ }
+
+ public static boolean isExceptionalLocale(final String localeString) {
+ return sExceptionalLocaleToNameIdsMap.containsKey(localeString);
+ }
+
+ private static final String getNoLanguageLayoutKey(final String keyboardLayoutName) {
+ return NO_LANGUAGE + "_" + keyboardLayoutName;
+ }
+
+ public static int getSubtypeNameId(final String localeString, final String keyboardLayoutName) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN
+ && isExceptionalLocale(localeString)) {
+ return sExceptionalLocaleToWithLayoutNameIdsMap.get(localeString);
+ }
+ final String key = NO_LANGUAGE.equals(localeString)
+ ? getNoLanguageLayoutKey(keyboardLayoutName)
+ : keyboardLayoutName;
+ final Integer nameId = sKeyboardLayoutToNameIdsMap.get(key);
+ return nameId == null ? UNKNOWN_KEYBOARD_LAYOUT : nameId;
+ }
+
+ private static Locale getDisplayLocaleOfSubtypeLocale(final String localeString) {
+ if (NO_LANGUAGE.equals(localeString)) {
+ return sResources.getConfiguration().locale;
+ }
+ return LocaleUtils.constructLocaleFromString(localeString);
+ }
+
+ public static String getSubtypeLocaleDisplayNameInSystemLocale(final String localeString) {
+ final Locale displayLocale = sResources.getConfiguration().locale;
+ return getSubtypeLocaleDisplayNameInternal(localeString, displayLocale);
+ }
+
+ public static String getSubtypeLocaleDisplayName(final String localeString) {
+ final Locale displayLocale = getDisplayLocaleOfSubtypeLocale(localeString);
+ return getSubtypeLocaleDisplayNameInternal(localeString, displayLocale);
+ }
+
+ private static String getSubtypeLocaleDisplayNameInternal(final String localeString,
+ final Locale displayLocale) {
+ final Integer exceptionalNameResId = sExceptionalLocaleToNameIdsMap.get(localeString);
+ final String displayName;
+ if (exceptionalNameResId != null) {
+ final RunInLocale<String> getExceptionalName = new RunInLocale<String>() {
+ @Override
+ protected String job(final Resources res) {
+ return res.getString(exceptionalNameResId);
+ }
+ };
+ displayName = getExceptionalName.runInLocale(sResources, displayLocale);
+ } else if (NO_LANGUAGE.equals(localeString)) {
+ // No language subtype should be displayed in system locale.
+ return sResources.getString(R.string.subtype_no_language);
+ } else {
+ final Locale locale = LocaleUtils.constructLocaleFromString(localeString);
+ displayName = locale.getDisplayName(displayLocale);
+ }
+ return StringUtils.capitalizeFirstCodePoint(displayName, displayLocale);
+ }
+
+ // InputMethodSubtype's display name in its locale.
+ // isAdditionalSubtype (T=true, F=false)
+ // locale layout | display name
+ // ------ ------- - ----------------------
+ // en_US qwerty F English (US) exception
+ // en_GB qwerty F English (UK) exception
+ // es_US spanish F Español (EE.UU.) exception
+ // fr azerty F Français
+ // fr_CA qwerty F Français (Canada)
+ // de qwertz F Deutsch
+ // zz qwerty F No language (QWERTY) in system locale
+ // fr qwertz T Français (QWERTZ)
+ // de qwerty T Deutsch (QWERTY)
+ // en_US azerty T English (US) (AZERTY) exception
+ // zz azerty T No language (AZERTY) in system locale
+
+ private static String getReplacementString(final InputMethodSubtype subtype,
+ final Locale displayLocale) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN
+ && subtype.containsExtraValueKey(UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME)) {
+ return subtype.getExtraValueOf(UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME);
+ } else {
+ return getSubtypeLocaleDisplayNameInternal(subtype.getLocale(), displayLocale);
+ }
+ }
+
+ public static String getSubtypeDisplayNameInSystemLocale(final InputMethodSubtype subtype) {
+ final Locale displayLocale = sResources.getConfiguration().locale;
+ return getSubtypeDisplayNameInternal(subtype, displayLocale);
+ }
+
+ public static String getSubtypeNameForLogging(final InputMethodSubtype subtype) {
+ if (subtype == null) {
+ return "<null subtype>";
+ }
+ return getSubtypeLocale(subtype) + "/" + getKeyboardLayoutSetName(subtype);
+ }
+
+ private static String getSubtypeDisplayNameInternal(final InputMethodSubtype subtype,
+ final Locale displayLocale) {
+ final String replacementString = getReplacementString(subtype, displayLocale);
+ final int nameResId = subtype.getNameResId();
+ final RunInLocale<String> getSubtypeName = new RunInLocale<String>() {
+ @Override
+ protected String job(final Resources res) {
+ try {
+ return res.getString(nameResId, replacementString);
+ } catch (Resources.NotFoundException e) {
+ // TODO: Remove this catch when InputMethodManager.getCurrentInputMethodSubtype
+ // is fixed.
+ Log.w(TAG, "Unknown subtype: mode=" + subtype.getMode()
+ + " nameResId=" + subtype.getNameResId()
+ + " locale=" + subtype.getLocale()
+ + " extra=" + subtype.getExtraValue()
+ + "\n" + DebugLogUtils.getStackTrace());
+ return "";
+ }
+ }
+ };
+ return StringUtils.capitalizeFirstCodePoint(
+ getSubtypeName.runInLocale(sResources, displayLocale), displayLocale);
+ }
+
+ public static boolean isNoLanguage(final InputMethodSubtype subtype) {
+ final String localeString = subtype.getLocale();
+ return NO_LANGUAGE.equals(localeString);
+ }
+
+ public static Locale getSubtypeLocale(final InputMethodSubtype subtype) {
+ final String localeString = subtype.getLocale();
+ return LocaleUtils.constructLocaleFromString(localeString);
+ }
+
+ public static String getKeyboardLayoutSetDisplayName(final InputMethodSubtype subtype) {
+ final String layoutName = getKeyboardLayoutSetName(subtype);
+ return getKeyboardLayoutSetDisplayName(layoutName);
+ }
+
+ public static String getKeyboardLayoutSetDisplayName(final String layoutName) {
+ return sKeyboardLayoutToDisplayNameMap.get(layoutName);
+ }
+
+ public static String getKeyboardLayoutSetName(final InputMethodSubtype subtype) {
+ String keyboardLayoutSet = subtype.getExtraValueOf(KEYBOARD_LAYOUT_SET);
+ if (keyboardLayoutSet == null) {
+ // This subtype doesn't have a keyboardLayoutSet extra value, so lookup its keyboard
+ // layout set in sLocaleAndExtraValueToKeyboardLayoutSetMap to keep it compatible with
+ // pre-JellyBean.
+ final String key = subtype.getLocale() + ":" + subtype.getExtraValue();
+ keyboardLayoutSet = sLocaleAndExtraValueToKeyboardLayoutSetMap.get(key);
+ }
+ // TODO: Remove this null check when InputMethodManager.getCurrentInputMethodSubtype is
+ // fixed.
+ if (keyboardLayoutSet == null) {
+ android.util.Log.w(TAG, "KeyboardLayoutSet not found, use QWERTY: " +
+ "locale=" + subtype.getLocale() + " extraValue=" + subtype.getExtraValue());
+ return QWERTY;
+ }
+ return keyboardLayoutSet;
+ }
+
+ // InputMethodSubtype's display name for spacebar text in its locale.
+ // isAdditionalSubtype (T=true, F=false)
+ // locale layout | Short Middle Full
+ // ------ ------- - ---- --------- ----------------------
+ // en_US qwerty F En English English (US) exception
+ // en_GB qwerty F En English English (UK) exception
+ // es_US spanish F Es Español Español (EE.UU.) exception
+ // fr azerty F Fr Français Français
+ // fr_CA qwerty F Fr Français Français (Canada)
+ // de qwertz F De Deutsch Deutsch
+ // zz qwerty F QWERTY QWERTY
+ // fr qwertz T Fr Français Français
+ // de qwerty T De Deutsch Deutsch
+ // en_US azerty T En English English (US)
+ // zz azerty T AZERTY AZERTY
+
+ // Get InputMethodSubtype's full display name in its locale.
+ public static String getFullDisplayName(final InputMethodSubtype subtype) {
+ if (isNoLanguage(subtype)) {
+ return getKeyboardLayoutSetDisplayName(subtype);
+ }
+ return getSubtypeLocaleDisplayName(subtype.getLocale());
+ }
+
+ // Get InputMethodSubtype's middle display name in its locale.
+ public static String getMiddleDisplayName(final InputMethodSubtype subtype) {
+ if (isNoLanguage(subtype)) {
+ return getKeyboardLayoutSetDisplayName(subtype);
+ }
+ final Locale locale = getSubtypeLocale(subtype);
+ return getSubtypeLocaleDisplayName(locale.getLanguage());
+ }
+
+ // Get InputMethodSubtype's short display name in its locale.
+ public static String getShortDisplayName(final InputMethodSubtype subtype) {
+ if (isNoLanguage(subtype)) {
+ return "";
+ }
+ final Locale locale = getSubtypeLocale(subtype);
+ return StringUtils.capitalizeFirstCodePoint(locale.getLanguage(), locale);
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/TargetPackageInfoGetterTask.java b/java/src/com/android/inputmethod/latin/utils/TargetPackageInfoGetterTask.java
new file mode 100644
index 000000000..afbe2ecad
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/TargetPackageInfoGetterTask.java
@@ -0,0 +1,70 @@
+/*
+ * 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.utils;
+
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.os.AsyncTask;
+import android.util.LruCache;
+
+public final class TargetPackageInfoGetterTask extends
+ AsyncTask<String, Void, PackageInfo> {
+ private static final int MAX_CACHE_ENTRIES = 64; // arbitrary
+ private static final LruCache<String, PackageInfo> sCache =
+ new LruCache<String, PackageInfo>(MAX_CACHE_ENTRIES);
+
+ public static PackageInfo getCachedPackageInfo(final String packageName) {
+ if (null == packageName) return null;
+ return sCache.get(packageName);
+ }
+
+ public static void removeCachedPackageInfo(final String packageName) {
+ sCache.remove(packageName);
+ }
+
+ public interface OnTargetPackageInfoKnownListener {
+ public void onTargetPackageInfoKnown(final PackageInfo info);
+ }
+
+ private Context mContext;
+ private final OnTargetPackageInfoKnownListener mListener;
+
+ public TargetPackageInfoGetterTask(final Context context,
+ final OnTargetPackageInfoKnownListener listener) {
+ mContext = context;
+ mListener = listener;
+ }
+
+ @Override
+ protected PackageInfo doInBackground(final String... packageName) {
+ final PackageManager pm = mContext.getPackageManager();
+ mContext = null; // Bazooka-powered anti-leak device
+ try {
+ final PackageInfo packageInfo = pm.getPackageInfo(packageName[0], 0 /* flags */);
+ sCache.put(packageName[0], packageInfo);
+ return packageInfo;
+ } catch (android.content.pm.PackageManager.NameNotFoundException e) {
+ return null;
+ }
+ }
+
+ @Override
+ protected void onPostExecute(final PackageInfo info) {
+ mListener.onTargetPackageInfoKnown(info);
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/TextRange.java b/java/src/com/android/inputmethod/latin/utils/TextRange.java
new file mode 100644
index 000000000..48b443ddd
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/TextRange.java
@@ -0,0 +1,120 @@
+/*
+ * 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.utils;
+
+import android.text.Spanned;
+import android.text.style.SuggestionSpan;
+
+import java.util.Arrays;
+
+/**
+ * Represents a range of text, relative to the current cursor position.
+ */
+public final class TextRange {
+ private final CharSequence mTextAtCursor;
+ private final int mWordAtCursorStartIndex;
+ private final int mWordAtCursorEndIndex;
+ private final int mCursorIndex;
+
+ public final CharSequence mWord;
+
+ public int getNumberOfCharsInWordBeforeCursor() {
+ return mCursorIndex - mWordAtCursorStartIndex;
+ }
+
+ public int getNumberOfCharsInWordAfterCursor() {
+ return mWordAtCursorEndIndex - mCursorIndex;
+ }
+
+ public int length() {
+ return mWord.length();
+ }
+
+ /**
+ * Gets the suggestion spans that are put squarely on the word, with the exact start
+ * and end of the span matching the boundaries of the word.
+ * @return the list of spans.
+ */
+ public SuggestionSpan[] getSuggestionSpansAtWord() {
+ if (!(mTextAtCursor instanceof Spanned && mWord instanceof Spanned)) {
+ return new SuggestionSpan[0];
+ }
+ final Spanned text = (Spanned)mTextAtCursor;
+ // Note: it's fine to pass indices negative or greater than the length of the string
+ // to the #getSpans() method. The reason we need to get from -1 to +1 is that, the
+ // spans were cut at the cursor position, and #getSpans(start, end) does not return
+ // spans that end at `start' or begin at `end'. Consider the following case:
+ // this| is (The | symbolizes the cursor position
+ // ---- ---
+ // In this case, the cursor is in position 4, so the 0~7 span has been split into
+ // a 0~4 part and a 4~7 part.
+ // If we called #getSpans(0, 4) in this case, we would only get the part from 0 to 4
+ // of the span, and not the part from 4 to 7, so we would not realize the span actually
+ // extends from 0 to 7. But if we call #getSpans(-1, 5) we'll get both the 0~4 and
+ // the 4~7 spans and we can merge them accordingly.
+ // Any span starting more than 1 char away from the word boundaries in any direction
+ // does not touch the word, so we don't need to consider it. That's why requesting
+ // -1 ~ +1 is enough.
+ // Of course this is only relevant if the cursor is at one end of the word. If it's
+ // in the middle, the -1 and +1 are not necessary, but they are harmless.
+ final SuggestionSpan[] spans = text.getSpans(mWordAtCursorStartIndex - 1,
+ mWordAtCursorEndIndex + 1, SuggestionSpan.class);
+ int readIndex = 0;
+ int writeIndex = 0;
+ for (; readIndex < spans.length; ++readIndex) {
+ final SuggestionSpan span = spans[readIndex];
+ // The span may be null, as we null them when we find duplicates. Cf a few lines
+ // down.
+ if (null == span) continue;
+ // Tentative span start and end. This may be modified later if we realize the
+ // same span is also applied to other parts of the string.
+ int spanStart = text.getSpanStart(span);
+ int spanEnd = text.getSpanEnd(span);
+ for (int i = readIndex + 1; i < spans.length; ++i) {
+ if (span.equals(spans[i])) {
+ // We found the same span somewhere else. Read the new extent of this
+ // span, and adjust our values accordingly.
+ spanStart = Math.min(spanStart, text.getSpanStart(spans[i]));
+ spanEnd = Math.max(spanEnd, text.getSpanEnd(spans[i]));
+ // ...and mark the span as processed.
+ spans[i] = null;
+ }
+ }
+ if (spanStart == mWordAtCursorStartIndex && spanEnd == mWordAtCursorEndIndex) {
+ // If the span does not start and stop here, we ignore it. It probably extends
+ // past the start or end of the word, as happens in missing space correction
+ // or EasyEditSpans put by voice input.
+ spans[writeIndex++] = spans[readIndex];
+ }
+ }
+ return writeIndex == readIndex ? spans : Arrays.copyOfRange(spans, 0, writeIndex);
+ }
+
+ public TextRange(final CharSequence textAtCursor, final int wordAtCursorStartIndex,
+ final int wordAtCursorEndIndex, final int cursorIndex) {
+ if (wordAtCursorStartIndex < 0 || cursorIndex < wordAtCursorStartIndex
+ || cursorIndex > wordAtCursorEndIndex
+ || wordAtCursorEndIndex > textAtCursor.length()) {
+ throw new IndexOutOfBoundsException();
+ }
+ mTextAtCursor = textAtCursor;
+ mWordAtCursorStartIndex = wordAtCursorStartIndex;
+ mWordAtCursorEndIndex = wordAtCursorEndIndex;
+ mCursorIndex = cursorIndex;
+ mWord = mTextAtCursor.subSequence(mWordAtCursorStartIndex, mWordAtCursorEndIndex);
+ }
+} \ No newline at end of file
diff --git a/java/src/com/android/inputmethod/latin/utils/TypefaceUtils.java b/java/src/com/android/inputmethod/latin/utils/TypefaceUtils.java
new file mode 100644
index 000000000..47ea1ea75
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/TypefaceUtils.java
@@ -0,0 +1,94 @@
+/*
+ * 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.utils;
+
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.util.SparseArray;
+
+public final class TypefaceUtils {
+ private TypefaceUtils() {
+ // This utility class is not publicly instantiable.
+ }
+
+ // This sparse array caches key label text height in pixel indexed by key label text size.
+ private static final SparseArray<Float> sTextHeightCache = CollectionUtils.newSparseArray();
+ // Working variable for the following method.
+ private static final Rect sTextHeightBounds = new Rect();
+
+ public static float getCharHeight(final char[] referenceChar, final Paint paint) {
+ final int key = getCharGeometryCacheKey(referenceChar[0], paint);
+ synchronized (sTextHeightCache) {
+ final Float cachedValue = sTextHeightCache.get(key);
+ if (cachedValue != null) {
+ return cachedValue;
+ }
+
+ paint.getTextBounds(referenceChar, 0, 1, sTextHeightBounds);
+ final float height = sTextHeightBounds.height();
+ sTextHeightCache.put(key, height);
+ return height;
+ }
+ }
+
+ // This sparse array caches key label text width in pixel indexed by key label text size.
+ private static final SparseArray<Float> sTextWidthCache = CollectionUtils.newSparseArray();
+ // Working variable for the following method.
+ private static final Rect sTextWidthBounds = new Rect();
+
+ public static float getCharWidth(final char[] referenceChar, final Paint paint) {
+ final int key = getCharGeometryCacheKey(referenceChar[0], paint);
+ synchronized (sTextWidthCache) {
+ final Float cachedValue = sTextWidthCache.get(key);
+ if (cachedValue != null) {
+ return cachedValue;
+ }
+
+ paint.getTextBounds(referenceChar, 0, 1, sTextWidthBounds);
+ final float width = sTextWidthBounds.width();
+ sTextWidthCache.put(key, width);
+ return width;
+ }
+ }
+
+ public static float getStringWidth(final String string, final Paint paint) {
+ paint.getTextBounds(string, 0, string.length(), sTextWidthBounds);
+ return sTextWidthBounds.width();
+ }
+
+ private static int getCharGeometryCacheKey(final char referenceChar, final Paint paint) {
+ final int labelSize = (int)paint.getTextSize();
+ final Typeface face = paint.getTypeface();
+ final int codePointOffset = referenceChar << 15;
+ if (face == Typeface.DEFAULT) {
+ return codePointOffset + labelSize;
+ } else if (face == Typeface.DEFAULT_BOLD) {
+ return codePointOffset + labelSize + 0x1000;
+ } else if (face == Typeface.MONOSPACE) {
+ return codePointOffset + labelSize + 0x2000;
+ } else {
+ return codePointOffset + labelSize;
+ }
+ }
+
+ public static float getLabelWidth(final String label, final Paint paint) {
+ final Rect textBounds = new Rect();
+ paint.getTextBounds(label, 0, label.length(), textBounds);
+ return textBounds.width();
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/UsabilityStudyLogUtils.java b/java/src/com/android/inputmethod/latin/utils/UsabilityStudyLogUtils.java
new file mode 100644
index 000000000..06826dac0
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/UsabilityStudyLogUtils.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2016 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.utils;
+
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.inputmethodservice.InputMethodService;
+import android.net.Uri;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Process;
+import android.util.Log;
+import android.view.MotionEvent;
+
+import com.android.inputmethod.latin.LatinImeLogger;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.nio.channels.FileChannel;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+public final class UsabilityStudyLogUtils {
+ // TODO: remove code duplication with ResearchLog class
+ private static final String USABILITY_TAG = UsabilityStudyLogUtils.class.getSimpleName();
+ private static final String FILENAME = "log.txt";
+ private final Handler mLoggingHandler;
+ private File mFile;
+ private File mDirectory;
+ private InputMethodService mIms;
+ private PrintWriter mWriter;
+ private final Date mDate;
+ private final SimpleDateFormat mDateFormat;
+
+ private UsabilityStudyLogUtils() {
+ mDate = new Date();
+ mDateFormat = new SimpleDateFormat("yyyyMMdd-HHmmss.SSSZ", Locale.US);
+
+ HandlerThread handlerThread = new HandlerThread("UsabilityStudyLogUtils logging task",
+ Process.THREAD_PRIORITY_BACKGROUND);
+ handlerThread.start();
+ mLoggingHandler = new Handler(handlerThread.getLooper());
+ }
+
+ // Initialization-on-demand holder
+ private static final class OnDemandInitializationHolder {
+ public static final UsabilityStudyLogUtils sInstance = new UsabilityStudyLogUtils();
+ }
+
+ public static UsabilityStudyLogUtils getInstance() {
+ return OnDemandInitializationHolder.sInstance;
+ }
+
+ public void init(final InputMethodService ims) {
+ mIms = ims;
+ mDirectory = ims.getFilesDir();
+ }
+
+ private void createLogFileIfNotExist() {
+ if ((mFile == null || !mFile.exists())
+ && (mDirectory != null && mDirectory.exists())) {
+ try {
+ mWriter = getPrintWriter(mDirectory, FILENAME, false);
+ } catch (final IOException e) {
+ Log.e(USABILITY_TAG, "Can't create log file.");
+ }
+ }
+ }
+
+ public static void writeBackSpace(final int x, final int y) {
+ UsabilityStudyLogUtils.getInstance().write("<backspace>\t" + x + "\t" + y);
+ }
+
+ public static void writeChar(final char c, final int x, final int y) {
+ String inputChar = String.valueOf(c);
+ switch (c) {
+ case '\n':
+ inputChar = "<enter>";
+ break;
+ case '\t':
+ inputChar = "<tab>";
+ break;
+ case ' ':
+ inputChar = "<space>";
+ break;
+ }
+ UsabilityStudyLogUtils.getInstance().write(inputChar + "\t" + x + "\t" + y);
+ LatinImeLogger.onPrintAllUsabilityStudyLogs();
+ }
+
+ public static void writeMotionEvent(final MotionEvent me) {
+ final int action = me.getActionMasked();
+ final long eventTime = me.getEventTime();
+ final int pointerCount = me.getPointerCount();
+ for (int index = 0; index < pointerCount; index++) {
+ final int id = me.getPointerId(index);
+ final int x = (int)me.getX(index);
+ final int y = (int)me.getY(index);
+ final float size = me.getSize(index);
+ final float pressure = me.getPressure(index);
+
+ final String eventTag;
+ switch (action) {
+ case MotionEvent.ACTION_UP:
+ eventTag = "[Up]";
+ break;
+ case MotionEvent.ACTION_DOWN:
+ eventTag = "[Down]";
+ break;
+ case MotionEvent.ACTION_POINTER_UP:
+ eventTag = "[PointerUp]";
+ break;
+ case MotionEvent.ACTION_POINTER_DOWN:
+ eventTag = "[PointerDown]";
+ break;
+ case MotionEvent.ACTION_MOVE:
+ eventTag = "[Move]";
+ break;
+ default:
+ eventTag = "[Action" + action + "]";
+ break;
+ }
+ getInstance().write(eventTag + eventTime + "," + id + "," + x + "," + y + "," + size
+ + "," + pressure);
+ }
+ }
+
+ public void write(final String log) {
+ mLoggingHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ createLogFileIfNotExist();
+ final long currentTime = System.currentTimeMillis();
+ mDate.setTime(currentTime);
+
+ final String printString = String.format(Locale.US, "%s\t%d\t%s\n",
+ mDateFormat.format(mDate), currentTime, log);
+ if (LatinImeLogger.sDBG) {
+ Log.d(USABILITY_TAG, "Write: " + log);
+ }
+ mWriter.print(printString);
+ }
+ });
+ }
+
+ private synchronized String getBufferedLogs() {
+ mWriter.flush();
+ final StringBuilder sb = new StringBuilder();
+ final BufferedReader br = getBufferedReader();
+ String line;
+ try {
+ while ((line = br.readLine()) != null) {
+ sb.append('\n');
+ sb.append(line);
+ }
+ } catch (final IOException e) {
+ Log.e(USABILITY_TAG, "Can't read log file.");
+ } finally {
+ if (LatinImeLogger.sDBG) {
+ Log.d(USABILITY_TAG, "Got all buffered logs\n" + sb.toString());
+ }
+ try {
+ br.close();
+ } catch (final IOException e) {
+ // ignore.
+ }
+ }
+ return sb.toString();
+ }
+
+ public void emailResearcherLogsAll() {
+ mLoggingHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ final Date date = new Date();
+ date.setTime(System.currentTimeMillis());
+ final String currentDateTimeString =
+ new SimpleDateFormat("yyyyMMdd-HHmmssZ", Locale.US).format(date);
+ if (mFile == null) {
+ Log.w(USABILITY_TAG, "No internal log file found.");
+ return;
+ }
+ if (mIms.checkCallingOrSelfPermission(
+ android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ != PackageManager.PERMISSION_GRANTED) {
+ Log.w(USABILITY_TAG, "Doesn't have the permission WRITE_EXTERNAL_STORAGE");
+ return;
+ }
+ mWriter.flush();
+ final String destPath = Environment.getExternalStorageDirectory()
+ + "/research-" + currentDateTimeString + ".log";
+ final File destFile = new File(destPath);
+ try {
+ final FileInputStream srcStream = new FileInputStream(mFile);
+ final FileOutputStream destStream = new FileOutputStream(destFile);
+ final FileChannel src = srcStream.getChannel();
+ final FileChannel dest = destStream.getChannel();
+ src.transferTo(0, src.size(), dest);
+ src.close();
+ srcStream.close();
+ dest.close();
+ destStream.close();
+ } catch (final FileNotFoundException e1) {
+ Log.w(USABILITY_TAG, e1);
+ return;
+ } catch (final IOException e2) {
+ Log.w(USABILITY_TAG, e2);
+ return;
+ }
+ if (!destFile.exists()) {
+ Log.w(USABILITY_TAG, "Dest file doesn't exist.");
+ return;
+ }
+ final Intent intent = new Intent(Intent.ACTION_SEND);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ if (LatinImeLogger.sDBG) {
+ Log.d(USABILITY_TAG, "Destination file URI is " + destFile.toURI());
+ }
+ intent.setType("text/plain");
+ intent.putExtra(Intent.EXTRA_STREAM, Uri.parse("file://" + destPath));
+ intent.putExtra(Intent.EXTRA_SUBJECT,
+ "[Research Logs] " + currentDateTimeString);
+ mIms.startActivity(intent);
+ }
+ });
+ }
+
+ public void printAll() {
+ mLoggingHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mIms.getCurrentInputConnection().commitText(getBufferedLogs(), 0);
+ }
+ });
+ }
+
+ public void clearAll() {
+ mLoggingHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (mFile != null && mFile.exists()) {
+ if (LatinImeLogger.sDBG) {
+ Log.d(USABILITY_TAG, "Delete log file.");
+ }
+ mFile.delete();
+ mWriter.close();
+ }
+ }
+ });
+ }
+
+ private BufferedReader getBufferedReader() {
+ createLogFileIfNotExist();
+ try {
+ return new BufferedReader(new FileReader(mFile));
+ } catch (final FileNotFoundException e) {
+ return null;
+ }
+ }
+
+ private PrintWriter getPrintWriter(final File dir, final String filename,
+ final boolean renew) throws IOException {
+ mFile = new File(dir, filename);
+ if (mFile.exists()) {
+ if (renew) {
+ mFile.delete();
+ }
+ }
+ return new PrintWriter(new FileOutputStream(mFile), true /* autoFlush */);
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java b/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java
new file mode 100644
index 000000000..ea32a74ff
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java
@@ -0,0 +1,173 @@
+/*
+ * 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.utils;
+
+import android.util.Log;
+
+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.FormatOptions;
+import com.android.inputmethod.latin.makedict.FusionDictionary;
+import com.android.inputmethod.latin.makedict.FusionDictionary.PtNodeArray;
+import com.android.inputmethod.latin.makedict.PendingAttribute;
+import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
+import com.android.inputmethod.latin.personalization.UserHistoryDictionaryBigramList;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map.Entry;
+import java.util.TreeMap;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Reads and writes Binary files for a UserHistoryDictionary.
+ *
+ * All the methods in this class are static.
+ */
+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 {
+ public void setUnigram(final String word, final String shortcutTarget, final int frequency);
+ public void setBigram(final String word1, final String word2, final int frequency);
+ }
+
+ @UsedForTesting
+ public interface BigramDictionaryInterface {
+ public int getFrequency(final String word1, final String word2);
+ }
+
+ /**
+ * Writes dictionary to file.
+ */
+ public static void writeDictionary(final DictEncoder dictEncoder,
+ final BigramDictionaryInterface dict, final UserHistoryDictionaryBigramList bigrams,
+ final FormatOptions formatOptions) {
+ final FusionDictionary fusionDict = constructFusionDictionary(dict, bigrams);
+ fusionDict.addOptionAttribute(USES_FORGETTING_CURVE_KEY, USES_FORGETTING_CURVE_VALUE);
+ fusionDict.addOptionAttribute(LAST_UPDATED_TIME_KEY,
+ String.valueOf(TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())));
+ try {
+ dictEncoder.writeDictionary(fusionDict, formatOptions);
+ Log.d(TAG, "end writing");
+ } catch (IOException e) {
+ Log.e(TAG, "IO exception while writing file", e);
+ } catch (UnsupportedFormatException e) {
+ Log.e(TAG, "Unsupported format", e);
+ }
+ }
+
+ /**
+ * Constructs a new FusionDictionary from BigramDictionaryInterface.
+ */
+ @UsedForTesting
+ static FusionDictionary constructFusionDictionary(
+ final BigramDictionaryInterface dict, final UserHistoryDictionaryBigramList bigrams) {
+ final FusionDictionary fusionDict = new FusionDictionary(new PtNodeArray(),
+ new FusionDictionary.DictionaryOptions(new HashMap<String, String>(), false,
+ false));
+ int profTotal = 0;
+ for (final String word1 : bigrams.keySet()) {
+ final HashMap<String, Byte> word1Bigrams = bigrams.getBigrams(word1);
+ for (final String word2 : word1Bigrams.keySet()) {
+ final int freq = dict.getFrequency(word1, word2);
+ if (freq == -1) {
+ // don't add this bigram.
+ continue;
+ }
+ if (DEBUG) {
+ if (word1 == null) {
+ Log.d(TAG, "add unigram: " + word2 + "," + Integer.toString(freq));
+ } else {
+ Log.d(TAG, "add bigram: " + word1
+ + "," + word2 + "," + Integer.toString(freq));
+ }
+ profTotal++;
+ }
+ if (word1 == null) { // unigram
+ fusionDict.add(word2, freq, null, false /* isNotAWord */);
+ } else { // bigram
+ if (FusionDictionary.findWordInTree(fusionDict.mRootNodeArray, word1) == null) {
+ fusionDict.add(word1, 2, null, false /* isNotAWord */);
+ }
+ fusionDict.setBigram(word1, word2, freq);
+ }
+ bigrams.updateBigram(word1, word2, (byte)freq);
+ }
+ }
+ if (DEBUG) {
+ Log.d(TAG, "add " + profTotal + "words");
+ }
+ return fusionDict;
+ }
+
+ /**
+ * Reads dictionary from file.
+ */
+ public static void readDictionaryBinary(final DictDecoder dictDecoder,
+ final OnAddWordListener dict) {
+ final TreeMap<Integer, String> unigrams = CollectionUtils.newTreeMap();
+ final TreeMap<Integer, Integer> frequencies = CollectionUtils.newTreeMap();
+ final TreeMap<Integer, ArrayList<PendingAttribute>> bigrams = CollectionUtils.newTreeMap();
+ try {
+ dictDecoder.readUnigramsAndBigramsBinary(unigrams, frequencies, bigrams);
+ } catch (IOException e) {
+ Log.e(TAG, "IO exception while reading file", e);
+ } catch (UnsupportedFormatException e) {
+ Log.e(TAG, "Unsupported format", e);
+ } catch (ArrayIndexOutOfBoundsException e) {
+ Log.e(TAG, "ArrayIndexOutOfBoundsException while reading file", e);
+ }
+ addWordsFromWordMap(unigrams, frequencies, bigrams, dict);
+ }
+
+ /**
+ * Adds all unigrams and bigrams in maps to OnAddWordListener.
+ */
+ @UsedForTesting
+ static void addWordsFromWordMap(final TreeMap<Integer, String> unigrams,
+ final TreeMap<Integer, Integer> frequencies,
+ final TreeMap<Integer, ArrayList<PendingAttribute>> bigrams,
+ final OnAddWordListener to) {
+ for (Entry<Integer, String> entry : unigrams.entrySet()) {
+ final String word1 = entry.getValue();
+ final int unigramFrequency = frequencies.get(entry.getKey());
+ to.setUnigram(word1, null, unigramFrequency);
+ final ArrayList<PendingAttribute> attrList = bigrams.get(entry.getKey());
+ if (attrList != null) {
+ for (final PendingAttribute attr : attrList) {
+ final String word2 = unigrams.get(attr.mAddress);
+ if (word1 == null || word2 == null) {
+ Log.e(TAG, "Invalid bigram pair detected: " + word1 + ", " + word2);
+ continue;
+ }
+ to.setBigram(word1, word2,
+ BinaryDictIOUtils.reconstructBigramFrequency(unigramFrequency,
+ attr.mFrequency));
+ }
+ }
+ }
+
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/UserHistoryForgettingCurveUtils.java b/java/src/com/android/inputmethod/latin/utils/UserHistoryForgettingCurveUtils.java
new file mode 100644
index 000000000..1992b2f5d
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/UserHistoryForgettingCurveUtils.java
@@ -0,0 +1,233 @@
+/*
+ * 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.utils;
+
+import android.util.Log;
+
+import java.util.concurrent.TimeUnit;
+
+public final class UserHistoryForgettingCurveUtils {
+ private static final String TAG = UserHistoryForgettingCurveUtils.class.getSimpleName();
+ private static final boolean DEBUG = false;
+ private static final int DEFAULT_FC_FREQ = 127;
+ private static final int BOOSTED_FC_FREQ = 200;
+ private static int FC_FREQ_MAX = DEFAULT_FC_FREQ;
+ /* package */ static final int COUNT_MAX = 3;
+ private static final int FC_LEVEL_MAX = 3;
+ /* package */ static final int ELAPSED_TIME_MAX = 15;
+ private static final int ELAPSED_TIME_INTERVAL_HOURS = 6;
+ private static final long ELAPSED_TIME_INTERVAL_MILLIS =
+ TimeUnit.HOURS.toMillis(ELAPSED_TIME_INTERVAL_HOURS);
+ private static final int HALF_LIFE_HOURS = 48;
+ private static final int MAX_PUSH_ELAPSED = (FC_LEVEL_MAX + 1) * (ELAPSED_TIME_MAX + 1);
+
+ public static void boostMaxFreqForDebug() {
+ FC_FREQ_MAX = BOOSTED_FC_FREQ;
+ }
+
+ public static void resetMaxFreqForDebug() {
+ FC_FREQ_MAX = DEFAULT_FC_FREQ;
+ }
+
+ private UserHistoryForgettingCurveUtils() {
+ // This utility class is not publicly instantiable.
+ }
+
+ public static final class ForgettingCurveParams {
+ private byte mFc;
+ long mLastTouchedTime = 0;
+ private final boolean mIsValid;
+
+ private void updateLastTouchedTime() {
+ mLastTouchedTime = System.currentTimeMillis();
+ }
+
+ public ForgettingCurveParams(boolean isValid) {
+ this(System.currentTimeMillis(), isValid);
+ }
+
+ private ForgettingCurveParams(long now, boolean isValid) {
+ this(pushCount((byte)0, isValid), now, now, isValid);
+ }
+
+ /** This constructor is called when the user history bigram dictionary is being restored. */
+ public ForgettingCurveParams(int fc, long now, long last) {
+ // All words with level >= 1 had been saved.
+ // Invalid words with level == 0 had been saved.
+ // Valid words words with level == 0 had *not* been saved.
+ this(fc, now, last, fcToLevel((byte)fc) > 0);
+ }
+
+ private ForgettingCurveParams(int fc, long now, long last, boolean isValid) {
+ mIsValid = isValid;
+ mFc = (byte)fc;
+ mLastTouchedTime = last;
+ updateElapsedTime(now);
+ }
+
+ public boolean isValid() {
+ return mIsValid;
+ }
+
+ public byte getFc() {
+ updateElapsedTime(System.currentTimeMillis());
+ return mFc;
+ }
+
+ public int getFrequency() {
+ updateElapsedTime(System.currentTimeMillis());
+ return UserHistoryForgettingCurveUtils.fcToFreq(mFc);
+ }
+
+ public int notifyTypedAgainAndGetFrequency() {
+ updateLastTouchedTime();
+ // TODO: Check whether this word is valid or not
+ mFc = pushCount(mFc, false);
+ return UserHistoryForgettingCurveUtils.fcToFreq(mFc);
+ }
+
+ private void updateElapsedTime(long now) {
+ final int elapsedTimeCount =
+ (int)((now - mLastTouchedTime) / ELAPSED_TIME_INTERVAL_MILLIS);
+ if (elapsedTimeCount <= 0) {
+ return;
+ }
+ if (elapsedTimeCount >= MAX_PUSH_ELAPSED) {
+ mLastTouchedTime = now;
+ mFc = 0;
+ return;
+ }
+ for (int i = 0; i < elapsedTimeCount; ++i) {
+ mLastTouchedTime += ELAPSED_TIME_INTERVAL_MILLIS;
+ mFc = pushElapsedTime(mFc);
+ }
+ }
+ }
+
+ /* package */ static int fcToElapsedTime(byte fc) {
+ return fc & 0x0F;
+ }
+
+ /* package */ static int fcToCount(byte fc) {
+ return (fc >> 4) & 0x03;
+ }
+
+ /* package */ static int fcToLevel(byte fc) {
+ return (fc >> 6) & 0x03;
+ }
+
+ private static int calcFreq(int elapsedTime, int count, int level) {
+ if (level <= 0) {
+ // Reserved words, just return -1
+ return -1;
+ }
+ if (count == COUNT_MAX) {
+ // Temporary promote because it's frequently typed recently
+ ++level;
+ }
+ final int et = Math.min(FC_FREQ_MAX, Math.max(0, elapsedTime));
+ final int l = Math.min(FC_LEVEL_MAX, Math.max(0, level));
+ return MathUtils.SCORE_TABLE[l - 1][et];
+ }
+
+ /* pakcage */ static byte calcFc(int elapsedTime, int count, int level) {
+ final int et = Math.min(FC_FREQ_MAX, Math.max(0, elapsedTime));
+ final int c = Math.min(COUNT_MAX, Math.max(0, count));
+ final int l = Math.min(FC_LEVEL_MAX, Math.max(0, level));
+ return (byte)(et | (c << 4) | (l << 6));
+ }
+
+ public static int fcToFreq(byte fc) {
+ final int elapsedTime = fcToElapsedTime(fc);
+ final int count = fcToCount(fc);
+ final int level = fcToLevel(fc);
+ return calcFreq(elapsedTime, count, level);
+ }
+
+ public static byte pushElapsedTime(byte fc) {
+ int elapsedTime = fcToElapsedTime(fc);
+ int count = fcToCount(fc);
+ int level = fcToLevel(fc);
+ if (elapsedTime >= ELAPSED_TIME_MAX) {
+ // Downgrade level
+ elapsedTime = 0;
+ count = COUNT_MAX;
+ --level;
+ } else {
+ ++elapsedTime;
+ }
+ return calcFc(elapsedTime, count, level);
+ }
+
+ public static byte pushCount(byte fc, boolean isValid) {
+ final int elapsedTime = fcToElapsedTime(fc);
+ int count = fcToCount(fc);
+ int level = fcToLevel(fc);
+ if ((elapsedTime == 0 && count >= COUNT_MAX) || (isValid && level == 0)) {
+ // Upgrade level
+ ++level;
+ count = 0;
+ if (DEBUG) {
+ Log.d(TAG, "Upgrade level.");
+ }
+ } else {
+ ++count;
+ }
+ return calcFc(0, count, level);
+ }
+
+ // TODO: isValid should be false for a word whose frequency is 0,
+ // or that is not in the dictionary.
+ /**
+ * Check wheather we should save the bigram to the SQL DB or not
+ */
+ public static boolean needsToSave(byte fc, boolean isValid, boolean addLevel0Bigram) {
+ int level = fcToLevel(fc);
+ if (level == 0) {
+ if (isValid || !addLevel0Bigram) {
+ return false;
+ }
+ }
+ final int elapsedTime = fcToElapsedTime(fc);
+ return (elapsedTime < ELAPSED_TIME_MAX - 1 || level > 0);
+ }
+
+ private static final class MathUtils {
+ public static final int[][] SCORE_TABLE = new int[FC_LEVEL_MAX][ELAPSED_TIME_MAX + 1];
+ static {
+ for (int i = 0; i < FC_LEVEL_MAX; ++i) {
+ final float initialFreq;
+ if (i >= 2) {
+ initialFreq = FC_FREQ_MAX;
+ } else if (i == 1) {
+ initialFreq = FC_FREQ_MAX / 2;
+ } else if (i == 0) {
+ initialFreq = FC_FREQ_MAX / 4;
+ } else {
+ continue;
+ }
+ for (int j = 0; j < ELAPSED_TIME_MAX; ++j) {
+ final float elapsedHours = j * ELAPSED_TIME_INTERVAL_HOURS;
+ final float freq = initialFreq
+ * (float)Math.pow(initialFreq, elapsedHours / HALF_LIFE_HOURS);
+ final int intFreq = Math.min(FC_FREQ_MAX, Math.max(0, (int)freq));
+ SCORE_TABLE[i][j] = intFreq;
+ }
+ }
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/UserLogRingCharBuffer.java b/java/src/com/android/inputmethod/latin/utils/UserLogRingCharBuffer.java
new file mode 100644
index 000000000..a75d353c9
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/UserLogRingCharBuffer.java
@@ -0,0 +1,137 @@
+/*
+ * 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.utils;
+
+import android.inputmethodservice.InputMethodService;
+
+import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.latin.LatinImeLogger;
+import com.android.inputmethod.latin.settings.Settings;
+
+public final class UserLogRingCharBuffer {
+ public /* for test */ static final int BUFSIZE = 20;
+ public /* for test */ int mLength = 0;
+
+ private static UserLogRingCharBuffer sUserLogRingCharBuffer = new UserLogRingCharBuffer();
+ private static final char PLACEHOLDER_DELIMITER_CHAR = '\uFFFC';
+ private static final int INVALID_COORDINATE = -2;
+ private boolean mEnabled = false;
+ private int mEnd = 0;
+ private char[] mCharBuf = new char[BUFSIZE];
+ private int[] mXBuf = new int[BUFSIZE];
+ private int[] mYBuf = new int[BUFSIZE];
+
+ private UserLogRingCharBuffer() {
+ // Intentional empty constructor for singleton.
+ }
+
+ @UsedForTesting
+ public static UserLogRingCharBuffer getInstance() {
+ return sUserLogRingCharBuffer;
+ }
+
+ public static UserLogRingCharBuffer init(final InputMethodService context,
+ final boolean enabled, final boolean usabilityStudy) {
+ if (!(enabled || usabilityStudy)) {
+ return null;
+ }
+ sUserLogRingCharBuffer.mEnabled = true;
+ UsabilityStudyLogUtils.getInstance().init(context);
+ return sUserLogRingCharBuffer;
+ }
+
+ private static int normalize(final int in) {
+ int ret = in % BUFSIZE;
+ return ret < 0 ? ret + BUFSIZE : ret;
+ }
+
+ // TODO: accept code points
+ @UsedForTesting
+ public void push(final char c, final int x, final int y) {
+ if (!mEnabled) {
+ return;
+ }
+ if (LatinImeLogger.sUsabilityStudy) {
+ UsabilityStudyLogUtils.getInstance().writeChar(c, x, y);
+ }
+ mCharBuf[mEnd] = c;
+ mXBuf[mEnd] = x;
+ mYBuf[mEnd] = y;
+ mEnd = normalize(mEnd + 1);
+ if (mLength < BUFSIZE) {
+ ++mLength;
+ }
+ }
+
+ public char pop() {
+ if (mLength < 1) {
+ return PLACEHOLDER_DELIMITER_CHAR;
+ }
+ mEnd = normalize(mEnd - 1);
+ --mLength;
+ return mCharBuf[mEnd];
+ }
+
+ public char getBackwardNthChar(final int n) {
+ if (mLength <= n || n < 0) {
+ return PLACEHOLDER_DELIMITER_CHAR;
+ }
+ return mCharBuf[normalize(mEnd - n - 1)];
+ }
+
+ public int getPreviousX(final char c, final int back) {
+ final int index = normalize(mEnd - 2 - back);
+ if (mLength <= back
+ || Character.toLowerCase(c) != Character.toLowerCase(mCharBuf[index])) {
+ return INVALID_COORDINATE;
+ }
+ return mXBuf[index];
+ }
+
+ public int getPreviousY(final char c, final int back) {
+ int index = normalize(mEnd - 2 - back);
+ if (mLength <= back
+ || Character.toLowerCase(c) != Character.toLowerCase(mCharBuf[index])) {
+ return INVALID_COORDINATE;
+ }
+ return mYBuf[index];
+ }
+
+ public String getLastWord(final int ignoreCharCount) {
+ final StringBuilder sb = new StringBuilder();
+ int i = ignoreCharCount;
+ for (; i < mLength; ++i) {
+ final char c = mCharBuf[normalize(mEnd - 1 - i)];
+ if (!Settings.getInstance().isWordSeparator(c)) {
+ break;
+ }
+ }
+ for (; i < mLength; ++i) {
+ char c = mCharBuf[normalize(mEnd - 1 - i)];
+ if (!Settings.getInstance().isWordSeparator(c)) {
+ sb.append(c);
+ } else {
+ break;
+ }
+ }
+ return sb.reverse().toString();
+ }
+
+ public void reset() {
+ mLength = 0;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/ViewLayoutUtils.java b/java/src/com/android/inputmethod/latin/utils/ViewLayoutUtils.java
new file mode 100644
index 000000000..f9d853493
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/ViewLayoutUtils.java
@@ -0,0 +1,54 @@
+/*
+ * 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.utils;
+
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.MarginLayoutParams;
+import android.widget.FrameLayout;
+import android.widget.RelativeLayout;
+
+public final class ViewLayoutUtils {
+ private ViewLayoutUtils() {
+ // This utility class is not publicly instantiable.
+ }
+
+ public static MarginLayoutParams newLayoutParam(final ViewGroup placer, final int width,
+ final int height) {
+ if (placer instanceof FrameLayout) {
+ return new FrameLayout.LayoutParams(width, height);
+ } else if (placer instanceof RelativeLayout) {
+ return new RelativeLayout.LayoutParams(width, height);
+ } else if (placer == null) {
+ throw new NullPointerException("placer is null");
+ } else {
+ throw new IllegalArgumentException("placer is neither FrameLayout nor RelativeLayout: "
+ + placer.getClass().getName());
+ }
+ }
+
+ public static void placeViewAt(final View view, final int x, final int y, final int w,
+ final int h) {
+ final ViewGroup.LayoutParams lp = view.getLayoutParams();
+ if (lp instanceof MarginLayoutParams) {
+ final MarginLayoutParams marginLayoutParams = (MarginLayoutParams)lp;
+ marginLayoutParams.width = w;
+ marginLayoutParams.height = h;
+ marginLayoutParams.setMargins(x, y, 0, 0);
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/XmlParseUtils.java b/java/src/com/android/inputmethod/latin/utils/XmlParseUtils.java
new file mode 100644
index 000000000..bdad16652
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/XmlParseUtils.java
@@ -0,0 +1,83 @@
+/*
+ * 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.utils;
+
+import android.content.res.TypedArray;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+
+public final class XmlParseUtils {
+ private XmlParseUtils() {
+ // This utility class is not publicly instantiable.
+ }
+
+ @SuppressWarnings("serial")
+ public static class ParseException extends XmlPullParserException {
+ public ParseException(final String msg, final XmlPullParser parser) {
+ super(msg + " at " + parser.getPositionDescription());
+ }
+ }
+
+ @SuppressWarnings("serial")
+ public static final class IllegalStartTag extends ParseException {
+ public IllegalStartTag(final XmlPullParser parser, final String tag, final String parent) {
+ super("Illegal start tag " + tag + " in " + parent, parser);
+ }
+ }
+
+ @SuppressWarnings("serial")
+ public static final class IllegalEndTag extends ParseException {
+ public IllegalEndTag(final XmlPullParser parser, final String tag, final String parent) {
+ super("Illegal end tag " + tag + " in " + parent, parser);
+ }
+ }
+
+ @SuppressWarnings("serial")
+ public static final class IllegalAttribute extends ParseException {
+ public IllegalAttribute(final XmlPullParser parser, final String tag,
+ final String attribute) {
+ super("Tag " + tag + " has illegal attribute " + attribute, parser);
+ }
+ }
+
+ @SuppressWarnings("serial")
+ public static final class NonEmptyTag extends ParseException{
+ public NonEmptyTag(final XmlPullParser parser, final String tag) {
+ super(tag + " must be empty tag", parser);
+ }
+ }
+
+ public static void checkEndTag(final String tag, final XmlPullParser parser)
+ throws XmlPullParserException, IOException {
+ if (parser.next() == XmlPullParser.END_TAG && tag.equals(parser.getName()))
+ return;
+ throw new NonEmptyTag(parser, tag);
+ }
+
+ public static void checkAttributeExists(final TypedArray attr, final int attrId,
+ final String attrName, final String tag, final XmlPullParser parser)
+ throws XmlPullParserException {
+ if (attr.hasValue(attrId)) {
+ return;
+ }
+ throw new ParseException(
+ "No " + attrName + " attribute found in <" + tag + "/>", parser);
+ }
+}