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