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/AdditionalFeaturesSettingUtils.java48
-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/ByteArrayDictBuffer.java81
-rw-r--r--java/src/com/android/inputmethod/latin/utils/CapsModeUtils.java11
-rw-r--r--java/src/com/android/inputmethod/latin/utils/CollectionUtils.java5
-rw-r--r--java/src/com/android/inputmethod/latin/utils/CsvUtils.java2
-rw-r--r--java/src/com/android/inputmethod/latin/utils/DebugLogUtils.java6
-rw-r--r--java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java16
-rw-r--r--java/src/com/android/inputmethod/latin/utils/FileTransforms.java38
-rw-r--r--java/src/com/android/inputmethod/latin/utils/LatinImeLoggerUtils.java77
-rw-r--r--java/src/com/android/inputmethod/latin/utils/LocaleUtils.java36
-rw-r--r--java/src/com/android/inputmethod/latin/utils/PositionalInfoForUserDictPendingAddition.java108
-rw-r--r--java/src/com/android/inputmethod/latin/utils/PrioritizedSerialExecutor.java147
-rw-r--r--java/src/com/android/inputmethod/latin/utils/ResizableIntArray.java9
-rw-r--r--java/src/com/android/inputmethod/latin/utils/ResourceUtils.java31
-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/StringUtils.java158
-rw-r--r--java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java333
-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.java49
-rw-r--r--java/src/com/android/inputmethod/latin/utils/UserHistoryForgettingCurveUtils.java19
-rw-r--r--java/src/com/android/inputmethod/latin/utils/UserLogRingCharBuffer.java137
-rw-r--r--java/src/com/android/inputmethod/latin/utils/Utils.java492
-rw-r--r--java/src/com/android/inputmethod/latin/utils/ViewLayoutUtils.java54
30 files changed, 2210 insertions, 734 deletions
diff --git a/java/src/com/android/inputmethod/latin/utils/AdditionalFeaturesSettingUtils.java b/java/src/com/android/inputmethod/latin/utils/AdditionalFeaturesSettingUtils.java
deleted file mode 100644
index 18dfb3dba..000000000
--- a/java/src/com/android/inputmethod/latin/utils/AdditionalFeaturesSettingUtils.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * 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.SharedPreferences;
-
-import com.android.inputmethod.latin.Settings;
-import com.android.inputmethodcommon.InputMethodSettingsFragment;
-
-/**
- * Utility class for managing additional features settings.
- */
-public class AdditionalFeaturesSettingUtils {
- public static final int ADDITIONAL_FEATURES_SETTINGS_SIZE = 0;
-
- private AdditionalFeaturesSettingUtils() {
- // This utility class is not publicly instantiable.
- }
-
- public static void addAdditionalFeaturesPreferences(
- final Context context, final InputMethodSettingsFragment settingsFragment) {
- // do nothing.
- }
-
- public static void readAdditionalFeaturesPreferencesIntoArray(
- final SharedPreferences prefs, final int[] additionalFeaturesPreferences) {
- // do nothing.
- }
-
- public static int[] getAdditionalNativeSuggestOptions() {
- return Settings.getInstance().getCurrent().mAdditionalFeaturesSettingValues;
- }
-}
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/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
index 2f91c5743..60b24d5d5 100644
--- a/java/src/com/android/inputmethod/latin/utils/CapsModeUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/CapsModeUtils.java
@@ -60,6 +60,11 @@ public final class CapsModeUtils {
|| 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
@@ -190,7 +195,7 @@ public final class CapsModeUtils {
if (c == Constants.CODE_QUESTION_MARK || c == Constants.CODE_EXCLAMATION_MARK) {
return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_SENTENCES) & reqModes;
}
- if (c != Constants.CODE_PERIOD || j <= 0) {
+ if (!isPeriod(c) || j <= 0) {
return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & reqModes;
}
@@ -240,7 +245,7 @@ public final class CapsModeUtils {
case WORD:
if (Character.isLetter(c)) {
state = WORD;
- } else if (c == Constants.CODE_PERIOD) {
+ } else if (isPeriod(c)) {
state = PERIOD;
} else {
return caps;
@@ -256,7 +261,7 @@ public final class CapsModeUtils {
case LETTER:
if (Character.isLetter(c)) {
state = LETTER;
- } else if (c == Constants.CODE_PERIOD) {
+ } else if (isPeriod(c)) {
state = PERIOD;
} else {
return noCaps;
diff --git a/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java b/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java
index 98f0d8b68..cc25102ce 100644
--- a/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java
@@ -18,6 +18,7 @@ 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;
@@ -94,6 +95,10 @@ public final class CollectionUtils {
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/CsvUtils.java b/java/src/com/android/inputmethod/latin/utils/CsvUtils.java
index 159ebb1b9..36b927eea 100644
--- a/java/src/com/android/inputmethod/latin/utils/CsvUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/CsvUtils.java
@@ -22,7 +22,7 @@ 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)},
+ * 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
diff --git a/java/src/com/android/inputmethod/latin/utils/DebugLogUtils.java b/java/src/com/android/inputmethod/latin/utils/DebugLogUtils.java
index c4ead0ad1..ac654fa65 100644
--- a/java/src/com/android/inputmethod/latin/utils/DebugLogUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/DebugLogUtils.java
@@ -65,12 +65,12 @@ public final class DebugLogUtils {
/**
* Get the stack trace contained in an exception as a human-readable string.
- * @param e the exception
+ * @param t the throwable
* @return the human-readable stack trace
*/
- public static String getStackTrace(final Exception e) {
+ public static String getStackTrace(final Throwable t) {
final StringBuilder sb = new StringBuilder();
- final StackTraceElement[] frames = e.getStackTrace();
+ final StackTraceElement[] frames = t.getStackTrace();
for (int j = 0; j < frames.length; ++j) {
sb.append(frames[j].toString() + "\n");
}
diff --git a/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java b/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java
index b3d37d78c..021bf0825 100644
--- a/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java
@@ -20,7 +20,6 @@ import android.content.ContentValues;
import android.content.Context;
import android.content.res.AssetManager;
import android.content.res.Resources;
-import android.text.format.DateUtils;
import android.util.Log;
import com.android.inputmethod.latin.AssetFileAddress;
@@ -28,13 +27,12 @@ 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 com.android.inputmethod.latin.makedict.UnsupportedFormatException;
import java.io.File;
-import java.io.IOException;
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.
@@ -74,8 +72,8 @@ public class DictionaryInfoUtils {
values.put(LOCALE_COLUMN, mLocale.toString());
values.put(DESCRIPTION_COLUMN, mDescription);
values.put(LOCAL_FILENAME_COLUMN, mFileAddress.mFilename);
- values.put(DATE_COLUMN,
- new File(mFileAddress.mFilename).lastModified() / DateUtils.SECOND_IN_MILLIS);
+ 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;
@@ -281,13 +279,7 @@ public class DictionaryInfoUtils {
}
public static FileHeader getDictionaryFileHeaderOrNull(final File file) {
- try {
- return BinaryDictIOUtils.getDictionaryFileHeader(file, 0, file.length());
- } catch (UnsupportedFormatException e) {
- return null;
- } catch (IOException e) {
- return null;
- }
+ return BinaryDictIOUtils.getDictionaryFileHeaderOrNull(file, 0, file.length());
}
private static DictionaryInfo createDictionaryInfoFromFileAddress(
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/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
index 58d062bbd..22045aa38 100644
--- a/java/src/com/android/inputmethod/latin/utils/LocaleUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/LocaleUtils.java
@@ -16,8 +16,6 @@
package com.android.inputmethod.latin.utils;
-import android.content.res.Configuration;
-import android.content.res.Resources;
import android.text.TextUtils;
import java.util.HashMap;
@@ -164,40 +162,6 @@ public final class LocaleUtils {
return LOCALE_MATCH <= level;
}
- static final Object sLockForRunInLocale = new Object();
-
- // TODO: Make this an external class
- public abstract static class RunInLocale<T> {
- protected abstract T job(Resources res);
-
- /**
- * Execute {@link #job(Resources)} method in specified system locale exclusively.
- *
- * @param res the resources to use. Pass current resources.
- * @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);
- }
- }
- }
- }
- }
-
private static final HashMap<String, Locale> sLocaleCache = CollectionUtils.newHashMap();
/**
diff --git a/java/src/com/android/inputmethod/latin/utils/PositionalInfoForUserDictPendingAddition.java b/java/src/com/android/inputmethod/latin/utils/PositionalInfoForUserDictPendingAddition.java
deleted file mode 100644
index 1fc7eccc6..000000000
--- a/java/src/com/android/inputmethod/latin/utils/PositionalInfoForUserDictPendingAddition.java
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * 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.view.inputmethod.EditorInfo;
-
-import com.android.inputmethod.latin.RichInputConnection;
-
-import java.util.Locale;
-
-/**
- * Holder class for data about a word already committed but that may still be edited.
- *
- * When the user chooses to add a word to the user dictionary by pressing the appropriate
- * suggestion, a dialog is presented to give a chance to edit the word before it is actually
- * registered as a user dictionary word. If the word is actually modified, the IME needs to
- * go back and replace the word that was committed with the amended version.
- * The word we need to replace with will only be known after it's actually committed, so
- * the IME needs to take a note of what it has to replace and where it is.
- * This class encapsulates this data.
- */
-public final class PositionalInfoForUserDictPendingAddition {
- final private String mOriginalWord;
- final private int mCursorPos; // Position of the cursor after the word
- final private EditorInfo mEditorInfo; // On what binding this has been added
- final private int mCapitalizedMode;
- private String mActualWordBeingAdded;
-
- public PositionalInfoForUserDictPendingAddition(final String word, final int cursorPos,
- final EditorInfo editorInfo, final int capitalizedMode) {
- mOriginalWord = word;
- mCursorPos = cursorPos;
- mEditorInfo = editorInfo;
- mCapitalizedMode = capitalizedMode;
- }
-
- public void setActualWordBeingAdded(final String actualWordBeingAdded) {
- mActualWordBeingAdded = actualWordBeingAdded;
- }
-
- /**
- * Try to replace the string at the remembered position with the actual word being added.
- *
- * After the user validated the word being added, the IME has to replace the old version
- * (which has been committed in the text view) with the amended version if it's different.
- * This method tries to do that, but may fail because the IME is not yet ready to do so -
- * for example, it is still waiting for the new string, or it is waiting to return to the text
- * view in which the amendment should be made. In these cases, we should keep the data
- * and wait until all conditions are met.
- * This method returns true if the replacement has been successfully made and this data
- * can be forgotten; it returns false if the replacement can't be made yet and we need to
- * keep this until a later time.
- * The IME knows about the actual word being added through a callback called by the
- * user dictionary facility of the device. When this callback comes, the keyboard may still
- * be connected to the edition dialog, or it may have already returned to the original text
- * field. Replacement has to work in both cases.
- * Accordingly, this method is called at two different points in time : upon getting the
- * event that a new word was added to the user dictionary, and upon starting up in a
- * new text field.
- * @param connection The RichInputConnection through which to contact the editor.
- * @param editorInfo Information pertaining to the editor we are currently in.
- * @param currentCursorPosition The current cursor position, for checking purposes.
- * @param locale The locale for changing case, if necessary
- * @return true if the edit has been successfully made, false if we need to try again later
- */
- public boolean tryReplaceWithActualWord(final RichInputConnection connection,
- final EditorInfo editorInfo, final int currentCursorPosition, final Locale locale) {
- // If we still don't know the actual word being added, we need to try again later.
- if (null == mActualWordBeingAdded) return false;
- // The entered text and the registered text were the same anyway : we can
- // return success right away even if focus has not returned yet to the text field we
- // want to amend.
- if (mActualWordBeingAdded.equals(mOriginalWord)) return true;
- // Not the same text field : we need to try again later. This happens when the addition
- // is reported by the user dictionary provider before the focus has moved back to the
- // original text view, so the IME is still in the text view of the dialog and has no way to
- // edit the original text view at this time.
- if (!mEditorInfo.packageName.equals(editorInfo.packageName)
- || mEditorInfo.fieldId != editorInfo.fieldId) {
- return false;
- }
- // Same text field, but not the same cursor position : we give up, so we return success
- // so that it won't be tried again
- if (currentCursorPosition != mCursorPos) return true;
- // We have made all the checks : do the replacement and report success
- // If this was auto-capitalized, we need to restore the case before committing
- final String wordWithCaseFixed = CapsModeUtils.applyAutoCapsMode(mActualWordBeingAdded,
- mCapitalizedMode, locale);
- connection.setComposingRegion(currentCursorPosition - mOriginalWord.length(),
- currentCursorPosition);
- connection.commitText(wordWithCaseFixed, wordWithCaseFixed.length());
- return true;
- }
-}
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/ResizableIntArray.java b/java/src/com/android/inputmethod/latin/utils/ResizableIntArray.java
index 4c7739a7a..7c6fe93ac 100644
--- a/java/src/com/android/inputmethod/latin/utils/ResizableIntArray.java
+++ b/java/src/com/android/inputmethod/latin/utils/ResizableIntArray.java
@@ -132,6 +132,15 @@ public final class ResizableIntArray {
}
}
+ /**
+ * 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();
diff --git a/java/src/com/android/inputmethod/latin/utils/ResourceUtils.java b/java/src/com/android/inputmethod/latin/utils/ResourceUtils.java
index ffec57548..22c92446a 100644
--- a/java/src/com/android/inputmethod/latin/utils/ResourceUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/ResourceUtils.java
@@ -20,10 +20,12 @@ 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;
@@ -215,6 +217,35 @@ public final class ResourceUtils {
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;
}
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/StringUtils.java b/java/src/com/android/inputmethod/latin/utils/StringUtils.java
index 7406d855a..121aecf0f 100644
--- a/java/src/com/android/inputmethod/latin/utils/StringUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/StringUtils.java
@@ -16,14 +16,25 @@
package com.android.inputmethod.latin.utils;
-import android.text.TextUtils;
-
+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
@@ -193,27 +204,56 @@ public final class StringUtils {
}
public static boolean isIdenticalAfterUpcase(final String text) {
- final int len = text.length();
- for (int i = 0; i < len; i = text.offsetByCodePoints(i, 1)) {
+ 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 len = text.length();
- for (int i = 0; i < len; i = text.offsetByCodePoints(i, 1)) {
+ 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;
@@ -316,4 +356,110 @@ public final class StringUtils {
// 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/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
index 32eb0b2c5..ea32a74ff 100644
--- a/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java
@@ -19,21 +19,22 @@ package com.android.inputmethod.latin.utils;
import android.util.Log;
import com.android.inputmethod.annotations.UsedForTesting;
-import com.android.inputmethod.latin.UserHistoryDictionaryBigramList;
import com.android.inputmethod.latin.makedict.BinaryDictIOUtils;
-import com.android.inputmethod.latin.makedict.BinaryDictInputOutput;
-import com.android.inputmethod.latin.makedict.BinaryDictInputOutput.FusionDictionaryBufferInterface;
+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.Node;
+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.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
-import java.util.Map;
+import java.util.Map.Entry;
+import java.util.TreeMap;
+import java.util.concurrent.TimeUnit;
/**
* Reads and writes Binary files for a UserHistoryDictionary.
@@ -43,6 +44,9 @@ import java.util.Map;
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);
@@ -57,12 +61,15 @@ public final class UserHistoryDictIOUtils {
/**
* Writes dictionary to file.
*/
- public static void writeDictionaryBinary(final OutputStream destination,
+ 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 {
- BinaryDictInputOutput.writeDictionaryBinary(destination, fusionDict, formatOptions);
+ dictEncoder.writeDictionary(fusionDict, formatOptions);
Log.d(TAG, "end writing");
} catch (IOException e) {
Log.e(TAG, "IO exception while writing file", e);
@@ -77,7 +84,7 @@ public final class UserHistoryDictIOUtils {
@UsedForTesting
static FusionDictionary constructFusionDictionary(
final BigramDictionaryInterface dict, final UserHistoryDictionaryBigramList bigrams) {
- final FusionDictionary fusionDict = new FusionDictionary(new Node(),
+ final FusionDictionary fusionDict = new FusionDictionary(new PtNodeArray(),
new FusionDictionary.DictionaryOptions(new HashMap<String, String>(), false,
false));
int profTotal = 0;
@@ -101,7 +108,7 @@ public final class UserHistoryDictIOUtils {
if (word1 == null) { // unigram
fusionDict.add(word2, freq, null, false /* isNotAWord */);
} else { // bigram
- if (FusionDictionary.findWordInTree(fusionDict.mRoot, word1) == null) {
+ if (FusionDictionary.findWordInTree(fusionDict.mRootNodeArray, word1) == null) {
fusionDict.add(word1, 2, null, false /* isNotAWord */);
}
fusionDict.setBigram(word1, word2, freq);
@@ -118,14 +125,13 @@ public final class UserHistoryDictIOUtils {
/**
* Reads dictionary from file.
*/
- public static void readDictionaryBinary(final FusionDictionaryBufferInterface buffer,
+ public static void readDictionaryBinary(final DictDecoder dictDecoder,
final OnAddWordListener dict) {
- final Map<Integer, String> unigrams = CollectionUtils.newTreeMap();
- final Map<Integer, Integer> frequencies = CollectionUtils.newTreeMap();
- final Map<Integer, ArrayList<PendingAttribute>> bigrams = CollectionUtils.newTreeMap();
+ final TreeMap<Integer, String> unigrams = CollectionUtils.newTreeMap();
+ final TreeMap<Integer, Integer> frequencies = CollectionUtils.newTreeMap();
+ final TreeMap<Integer, ArrayList<PendingAttribute>> bigrams = CollectionUtils.newTreeMap();
try {
- BinaryDictIOUtils.readUnigramsAndBigramsBinary(buffer, unigrams, frequencies,
- bigrams);
+ dictDecoder.readUnigramsAndBigramsBinary(unigrams, frequencies, bigrams);
} catch (IOException e) {
Log.e(TAG, "IO exception while reading file", e);
} catch (UnsupportedFormatException e) {
@@ -140,10 +146,11 @@ public final class UserHistoryDictIOUtils {
* Adds all unigrams and bigrams in maps to OnAddWordListener.
*/
@UsedForTesting
- static void addWordsFromWordMap(final Map<Integer, String> unigrams,
- final Map<Integer, Integer> frequencies,
- final Map<Integer, ArrayList<PendingAttribute>> bigrams, final OnAddWordListener to) {
- for (Map.Entry<Integer, String> entry : unigrams.entrySet()) {
+ 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);
@@ -156,7 +163,7 @@ public final class UserHistoryDictIOUtils {
continue;
}
to.setBigram(word1, word2,
- BinaryDictInputOutput.reconstructBigramFrequency(unigramFrequency,
+ 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
index 9f842f976..1992b2f5d 100644
--- a/java/src/com/android/inputmethod/latin/utils/UserHistoryForgettingCurveUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/UserHistoryForgettingCurveUtils.java
@@ -16,22 +16,33 @@
package com.android.inputmethod.latin.utils;
-import android.text.format.DateUtils;
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 FC_FREQ_MAX = 127;
+ 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 = ELAPSED_TIME_INTERVAL_HOURS
- * DateUtils.HOUR_IN_MILLIS;
+ 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.
}
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/Utils.java b/java/src/com/android/inputmethod/latin/utils/Utils.java
deleted file mode 100644
index 8070a6ea4..000000000
--- a/java/src/com/android/inputmethod/latin/utils/Utils.java
+++ /dev/null
@@ -1,492 +0,0 @@
-/*
- * Copyright (C) 2010 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.Intent;
-import android.content.pm.ActivityInfo;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.inputmethodservice.InputMethodService;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.os.Environment;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Process;
-import android.text.TextUtils;
-import android.util.Log;
-
-import com.android.inputmethod.annotations.UsedForTesting;
-import com.android.inputmethod.latin.Constants;
-import com.android.inputmethod.latin.LatinIME;
-import com.android.inputmethod.latin.LatinImeLogger;
-import com.android.inputmethod.latin.SuggestedWords;
-import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
-import com.android.inputmethod.latin.WordComposer;
-
-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;
-
-// TODO: Come up with a more descriptive class name
-public final class Utils {
- private static final String TAG = Utils.class.getSimpleName();
-
- private Utils() {
- // This utility class is not publicly instantiable.
- }
-
- // TODO: Make this an external class
- public /* for test */ static final class RingCharBuffer {
- public /* for test */ static final int BUFSIZE = 20;
- public /* for test */ int mLength = 0;
-
- private static RingCharBuffer sRingCharBuffer = new RingCharBuffer();
- private static final char PLACEHOLDER_DELIMITER_CHAR = '\uFFFC';
- private static final int INVALID_COORDINATE = -2;
- private InputMethodService mContext;
- 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 RingCharBuffer() {
- // Intentional empty constructor for singleton.
- }
-
- @UsedForTesting
- public static RingCharBuffer getInstance() {
- return sRingCharBuffer;
- }
-
- public static RingCharBuffer init(final InputMethodService context, final boolean enabled,
- final boolean usabilityStudy) {
- if (!(enabled || usabilityStudy)) {
- return null;
- }
- sRingCharBuffer.mContext = context;
- sRingCharBuffer.mEnabled = true;
- UsabilityStudyLogUtils.getInstance().init(context);
- return sRingCharBuffer;
- }
-
- 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;
- }
- 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();
- final LatinIME latinIme = (LatinIME)mContext;
- int i = ignoreCharCount;
- for (; i < mLength; ++i) {
- final char c = mCharBuf[normalize(mEnd - 1 - i)];
- if (!latinIme.isWordSeparator(c)) {
- break;
- }
- }
- for (; i < mLength; ++i) {
- char c = mCharBuf[normalize(mEnd - 1 - i)];
- if (!latinIme.isWordSeparator(c)) {
- sb.append(c);
- } else {
- break;
- }
- }
- return sb.reverse().toString();
- }
-
- public void reset() {
- mLength = 0;
- }
- }
-
- // TODO: Make this an external class
- public static 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 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 == null || !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 */);
- }
- }
-
- // TODO: Make this an external class
- public static final class Stats {
- public static void onNonSeparator(final char code, final int x, final int y) {
- RingCharBuffer.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
- RingCharBuffer.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();
- }
- }
-
- public static String getDebugInfo(final SuggestedWords suggestions, final int pos) {
- if (!LatinImeLogger.sDBG) {
- return null;
- }
- final SuggestedWordInfo wordInfo = suggestions.getInfo(pos);
- if (wordInfo == null) {
- return null;
- }
- final String info = wordInfo.getDebugString();
- if (TextUtils.isEmpty(info)) {
- return null;
- }
- return info;
- }
-
- 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/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);
+ }
+ }
+}