diff options
Diffstat (limited to 'java/src/com/android/inputmethod/latin/utils')
9 files changed, 299 insertions, 15 deletions
diff --git a/java/src/com/android/inputmethod/latin/utils/CursorAnchorInfoUtils.java b/java/src/com/android/inputmethod/latin/utils/CursorAnchorInfoUtils.java new file mode 100644 index 000000000..9dc0524a2 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/utils/CursorAnchorInfoUtils.java @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin.utils; + +import android.graphics.Matrix; +import android.graphics.Rect; +import android.inputmethodservice.ExtractEditText; +import android.inputmethodservice.InputMethodService; +import android.text.Layout; +import android.text.Spannable; +import android.view.View; +import android.view.ViewParent; +import android.view.inputmethod.CursorAnchorInfo; +import android.widget.TextView; + +/** + * This class allows input methods to extract {@link CursorAnchorInfo} directly from the given + * {@link TextView}. This is useful and even necessary to support full-screen mode where the default + * {@link InputMethodService#onUpdateCursorAnchorInfo(CursorAnchorInfo)} event callback must be + * ignored because it reports the character locations of the target application rather than + * characters on {@link ExtractEditText}. + */ +public final class CursorAnchorInfoUtils { + private CursorAnchorInfoUtils() { + // This helper class is not instantiable. + } + + private static boolean isPositionVisible(final View view, final float positionX, + final float positionY) { + final float[] position = new float[] { positionX, positionY }; + View currentView = view; + + while (currentView != null) { + if (currentView != view) { + // Local scroll is already taken into account in positionX/Y + position[0] -= currentView.getScrollX(); + position[1] -= currentView.getScrollY(); + } + + if (position[0] < 0 || position[1] < 0 || + position[0] > currentView.getWidth() || position[1] > currentView.getHeight()) { + return false; + } + + if (!currentView.getMatrix().isIdentity()) { + currentView.getMatrix().mapPoints(position); + } + + position[0] += currentView.getLeft(); + position[1] += currentView.getTop(); + + final ViewParent parent = currentView.getParent(); + if (parent instanceof View) { + currentView = (View) parent; + } else { + // We've reached the ViewRoot, stop iterating + currentView = null; + } + } + + // We've been able to walk up the view hierarchy and the position was never clipped + return true; + } + + /** + * Returns {@link CursorAnchorInfo} from the given {@link TextView}. + * @param textView the target text view from which {@link CursorAnchorInfo} is to be extracted. + * @return the {@link CursorAnchorInfo} object based on the current layout. {@code null} if it + * is not feasible. + */ + public static CursorAnchorInfo getCursorAnchorInfo(final TextView textView) { + Layout layout = textView.getLayout(); + if (layout == null) { + return null; + } + + final CursorAnchorInfo.Builder builder = new CursorAnchorInfo.Builder(); + + final int selectionStart = textView.getSelectionStart(); + builder.setSelectionRange(selectionStart, textView.getSelectionEnd()); + + // Construct transformation matrix from view local coordinates to screen coordinates. + final Matrix viewToScreenMatrix = new Matrix(textView.getMatrix()); + final int[] viewOriginInScreen = new int[2]; + textView.getLocationOnScreen(viewOriginInScreen); + viewToScreenMatrix.postTranslate(viewOriginInScreen[0], viewOriginInScreen[1]); + builder.setMatrix(viewToScreenMatrix); + + if (layout.getLineCount() == 0) { + return null; + } + final Rect lineBoundsWithoutOffset = new Rect(); + final Rect lineBoundsWithOffset = new Rect(); + layout.getLineBounds(0, lineBoundsWithoutOffset); + textView.getLineBounds(0, lineBoundsWithOffset); + final float viewportToContentHorizontalOffset = lineBoundsWithOffset.left + - lineBoundsWithoutOffset.left - textView.getScrollX(); + final float viewportToContentVerticalOffset = lineBoundsWithOffset.top + - lineBoundsWithoutOffset.top - textView.getScrollY(); + + final CharSequence text = textView.getText(); + if (text instanceof Spannable) { + // Here we assume that the composing text is marked as SPAN_COMPOSING flag. This is not + // necessarily true, but basically works. + int composingTextStart = text.length(); + int composingTextEnd = 0; + final Spannable spannable = (Spannable) text; + final Object[] spans = spannable.getSpans(0, text.length(), Object.class); + for (Object span : spans) { + final int spanFlag = spannable.getSpanFlags(span); + if ((spanFlag & Spannable.SPAN_COMPOSING) != 0) { + composingTextStart = Math.min(composingTextStart, + spannable.getSpanStart(span)); + composingTextEnd = Math.max(composingTextEnd, spannable.getSpanEnd(span)); + } + } + + final boolean hasComposingText = + (0 <= composingTextStart) && (composingTextStart < composingTextEnd); + if (hasComposingText) { + final CharSequence composingText = text.subSequence(composingTextStart, + composingTextEnd); + builder.setComposingText(composingTextStart, composingText); + + final int minLine = layout.getLineForOffset(composingTextStart); + final int maxLine = layout.getLineForOffset(composingTextEnd - 1); + for (int line = minLine; line <= maxLine; ++line) { + final int lineStart = layout.getLineStart(line); + final int lineEnd = layout.getLineEnd(line); + final int offsetStart = Math.max(lineStart, composingTextStart); + final int offsetEnd = Math.min(lineEnd, composingTextEnd); + final boolean ltrLine = + layout.getParagraphDirection(line) == Layout.DIR_LEFT_TO_RIGHT; + final float[] widths = new float[offsetEnd - offsetStart]; + layout.getPaint().getTextWidths(text, offsetStart, offsetEnd, widths); + final float top = layout.getLineTop(line); + final float bottom = layout.getLineBottom(line); + for (int offset = offsetStart; offset < offsetEnd; ++offset) { + final float charWidth = widths[offset - offsetStart]; + final boolean isRtl = layout.isRtlCharAt(offset); + final float primary = layout.getPrimaryHorizontal(offset); + final float secondary = layout.getSecondaryHorizontal(offset); + // TODO: This doesn't work perfectly for text with custom styles and TAB + // chars. + final float left; + final float right; + if (ltrLine) { + if (isRtl) { + left = secondary - charWidth; + right = secondary; + } else { + left = primary; + right = primary + charWidth; + } + } else { + if (!isRtl) { + left = secondary; + right = secondary + charWidth; + } else { + left = primary - charWidth; + right = primary; + } + } + // TODO: Check top-right and bottom-left as well. + final float localLeft = left + viewportToContentHorizontalOffset; + final float localRight = right + viewportToContentHorizontalOffset; + final float localTop = top + viewportToContentVerticalOffset; + final float localBottom = bottom + viewportToContentVerticalOffset; + final boolean isTopLeftVisible = isPositionVisible(textView, + localLeft, localTop); + final boolean isBottomRightVisible = + isPositionVisible(textView, localRight, localBottom); + int characterBoundsFlags = 0; + if (isTopLeftVisible || isBottomRightVisible) { + characterBoundsFlags |= CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION; + } + if (!isTopLeftVisible || !isTopLeftVisible) { + characterBoundsFlags |= CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION; + } + if (isRtl) { + characterBoundsFlags |= CursorAnchorInfo.FLAG_IS_RTL; + } + // Here offset is the index in Java chars. + builder.addCharacterBounds(offset, localLeft, localTop, localRight, + localBottom, characterBoundsFlags); + } + } + } + } + + // Treat selectionStart as the insertion point. + if (0 <= selectionStart) { + final int offset = selectionStart; + final int line = layout.getLineForOffset(offset); + final float insertionMarkerX = layout.getPrimaryHorizontal(offset) + + viewportToContentHorizontalOffset; + final float insertionMarkerTop = layout.getLineTop(line) + + viewportToContentVerticalOffset; + final float insertionMarkerBaseline = layout.getLineBaseline(line) + + viewportToContentVerticalOffset; + final float insertionMarkerBottom = layout.getLineBottom(line) + + viewportToContentVerticalOffset; + final boolean isTopVisible = + isPositionVisible(textView, insertionMarkerX, insertionMarkerTop); + final boolean isBottomVisible = + isPositionVisible(textView, insertionMarkerX, insertionMarkerBottom); + int insertionMarkerFlags = 0; + if (isTopVisible || isBottomVisible) { + insertionMarkerFlags |= CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION; + } + if (!isTopVisible || !isBottomVisible) { + insertionMarkerFlags |= CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION; + } + if (layout.isRtlCharAt(offset)) { + insertionMarkerFlags |= CursorAnchorInfo.FLAG_IS_RTL; + } + builder.setInsertionMarkerLocation(insertionMarkerX, insertionMarkerTop, + insertionMarkerBaseline, insertionMarkerBottom, insertionMarkerFlags); + } + return builder.build(); + } +} diff --git a/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java b/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java index 197908032..249478785 100644 --- a/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java @@ -23,6 +23,7 @@ import android.content.res.Resources; import android.text.TextUtils; import android.util.Log; +import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.latin.AssetFileAddress; import com.android.inputmethod.latin.BinaryDictionaryGetter; import com.android.inputmethod.latin.Constants; @@ -382,6 +383,7 @@ public class DictionaryInfoUtils { return dictList; } + @UsedForTesting public static boolean looksValidForDictionaryInsertion(final CharSequence text, final SpacingAndPunctuations spacingAndPunctuations) { if (TextUtils.isEmpty(text)) return false; diff --git a/java/src/com/android/inputmethod/latin/utils/DistracterFilter.java b/java/src/com/android/inputmethod/latin/utils/DistracterFilter.java index 94c62429e..6fd241ee9 100644 --- a/java/src/com/android/inputmethod/latin/utils/DistracterFilter.java +++ b/java/src/com/android/inputmethod/latin/utils/DistracterFilter.java @@ -21,6 +21,7 @@ import java.util.Locale; import android.view.inputmethod.InputMethodSubtype; +import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.latin.PrevWordsInfo; public interface DistracterFilter { @@ -36,6 +37,7 @@ public interface DistracterFilter { public boolean isDistracterToWordsInDictionaries(final PrevWordsInfo prevWordsInfo, final String testedWord, final Locale locale); + @UsedForTesting public int getWordHandlingType(final PrevWordsInfo prevWordsInfo, final String testedWord, final Locale locale); diff --git a/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingExactMatchesAndSuggestions.java b/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingExactMatchesAndSuggestions.java index 1db525502..f8a845304 100644 --- a/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingExactMatchesAndSuggestions.java +++ b/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingExactMatchesAndSuggestions.java @@ -64,9 +64,9 @@ public class DistracterFilterCheckingExactMatchesAndSuggestions implements Distr private final Object mLock = new Object(); // If the score of the top suggestion exceeds this value, the tested word (e.g., - // an OOV, a misspelling, or an in-vocabulary word) would be considered as a distractor to + // an OOV, a misspelling, or an in-vocabulary word) would be considered as a distracter to // words in dictionary. The greater the threshold is, the less likely the tested word would - // become a distractor, which means the tested word will be more likely to be added to + // become a distracter, which means the tested word will be more likely to be added to // the dictionary. private static final float DISTRACTER_WORD_SCORE_THRESHOLD = 0.4f; @@ -196,7 +196,7 @@ public class DistracterFilterCheckingExactMatchesAndSuggestions implements Distr } final boolean Word = dictionaryFacilitator.isValidWord(testedWord, false /* ignoreCase */); if (Word) { - // Valid word is not a distractor. + // Valid word is not a distracter. if (DEBUG) { Log.d(TAG, "isDistracter: false (valid word)"); } @@ -257,12 +257,12 @@ public class DistracterFilterCheckingExactMatchesAndSuggestions implements Distr return false; } final SuggestedWordInfo firstSuggestion = suggestionResults.first(); - final boolean isDistractor = suggestionExceedsDistracterThreshold( + final boolean isDistracter = suggestionExceedsDistracterThreshold( firstSuggestion, consideredWord, DISTRACTER_WORD_SCORE_THRESHOLD); if (DEBUG) { - Log.d(TAG, "isDistracter: " + isDistractor); + Log.d(TAG, "isDistracter: " + isDistracter); } - return isDistractor; + return isDistracter; } private static boolean suggestionExceedsDistracterThreshold(final SuggestedWordInfo suggestion, diff --git a/java/src/com/android/inputmethod/latin/utils/FragmentUtils.java b/java/src/com/android/inputmethod/latin/utils/FragmentUtils.java index 08f5b0b41..ae2de44c7 100644 --- a/java/src/com/android/inputmethod/latin/utils/FragmentUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/FragmentUtils.java @@ -18,13 +18,15 @@ package com.android.inputmethod.latin.utils; import com.android.inputmethod.dictionarypack.DictionarySettingsFragment; import com.android.inputmethod.latin.about.AboutPreferences; +import com.android.inputmethod.latin.settings.AccountsSettingsFragment; import com.android.inputmethod.latin.settings.AdvancedSettingsFragment; +import com.android.inputmethod.latin.settings.AppearanceSettingsFragment; import com.android.inputmethod.latin.settings.CorrectionSettingsFragment; import com.android.inputmethod.latin.settings.CustomInputStyleSettingsFragment; import com.android.inputmethod.latin.settings.DebugSettingsFragment; import com.android.inputmethod.latin.settings.GestureSettingsFragment; -import com.android.inputmethod.latin.settings.InputSettingsFragment; import com.android.inputmethod.latin.settings.MultiLingualSettingsFragment; +import com.android.inputmethod.latin.settings.PreferencesSettingsFragment; import com.android.inputmethod.latin.settings.SettingsFragment; import com.android.inputmethod.latin.settings.ThemeSettingsFragment; import com.android.inputmethod.latin.spellcheck.SpellCheckerSettingsFragment; @@ -40,7 +42,9 @@ public class FragmentUtils { static { sLatinImeFragments.add(DictionarySettingsFragment.class.getName()); sLatinImeFragments.add(AboutPreferences.class.getName()); - sLatinImeFragments.add(InputSettingsFragment.class.getName()); + sLatinImeFragments.add(PreferencesSettingsFragment.class.getName()); + sLatinImeFragments.add(AccountsSettingsFragment.class.getName()); + sLatinImeFragments.add(AppearanceSettingsFragment.class.getName()); sLatinImeFragments.add(ThemeSettingsFragment.class.getName()); sLatinImeFragments.add(MultiLingualSettingsFragment.class.getName()); sLatinImeFragments.add(CustomInputStyleSettingsFragment.class.getName()); diff --git a/java/src/com/android/inputmethod/latin/utils/LanguageModelParam.java b/java/src/com/android/inputmethod/latin/utils/LanguageModelParam.java index 05d124764..7955541aa 100644 --- a/java/src/com/android/inputmethod/latin/utils/LanguageModelParam.java +++ b/java/src/com/android/inputmethod/latin/utils/LanguageModelParam.java @@ -18,6 +18,7 @@ package com.android.inputmethod.latin.utils; import android.util.Log; +import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.latin.Dictionary; import com.android.inputmethod.latin.DictionaryFacilitator; import com.android.inputmethod.latin.PrevWordsInfo; @@ -58,12 +59,14 @@ public final class LanguageModelParam { public final int mTimestamp; // Constructor for unigram. TODO: support shortcuts + @UsedForTesting public LanguageModelParam(final CharSequence word, final int unigramProbability, final int timestamp) { this(null /* word0 */, word, unigramProbability, Dictionary.NOT_A_PROBABILITY, timestamp); } // Constructor for unigram and bigram. + @UsedForTesting public LanguageModelParam(final CharSequence word0, final CharSequence word1, final int unigramProbability, final int bigramProbability, final int timestamp) { diff --git a/java/src/com/android/inputmethod/latin/utils/PrevWordsInfoUtils.java b/java/src/com/android/inputmethod/latin/utils/PrevWordsInfoUtils.java index 3cd63612c..5720d9388 100644 --- a/java/src/com/android/inputmethod/latin/utils/PrevWordsInfoUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/PrevWordsInfoUtils.java @@ -16,6 +16,7 @@ package com.android.inputmethod.latin.utils; +import java.util.Arrays; import java.util.regex.Pattern; import com.android.inputmethod.latin.Constants; @@ -56,6 +57,7 @@ public final class PrevWordsInfoUtils { if (prev == null) return PrevWordsInfo.EMPTY_PREV_WORDS_INFO; final String[] w = SPACE_REGEX.split(prev); final WordInfo[] prevWordsInfo = new WordInfo[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM]; + Arrays.fill(prevWordsInfo, WordInfo.EMPTY_WORD_INFO); for (int i = 0; i < prevWordsInfo.length; i++) { final int focusedWordIndex = w.length - n - i; // Referring to the word after the focused word. @@ -66,7 +68,6 @@ public final class PrevWordsInfoUtils { if (spacingAndPunctuations.isWordConnector(firstChar)) { // The word following the focused word is starting with a word connector. // TODO: Return meaningful context for this case. - prevWordsInfo[i] = WordInfo.EMPTY_WORD_INFO; break; } } @@ -93,7 +94,6 @@ public final class PrevWordsInfoUtils { // TODO: Return meaningful context for this case. if (spacingAndPunctuations.isWordSeparator(lastChar) || spacingAndPunctuations.isWordConnector(lastChar)) { - prevWordsInfo[i] = WordInfo.EMPTY_WORD_INFO; break; } prevWordsInfo[i] = new WordInfo(focusedWord); diff --git a/java/src/com/android/inputmethod/latin/utils/StringUtils.java b/java/src/com/android/inputmethod/latin/utils/StringUtils.java index 55557de9d..bbcef990d 100644 --- a/java/src/com/android/inputmethod/latin/utils/StringUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/StringUtils.java @@ -37,6 +37,14 @@ public final class StringUtils { private static final String EMPTY_STRING = ""; + private static final char CHAR_LINE_FEED = 0X000A; + private static final char CHAR_VERTICAL_TAB = 0X000B; + private static final char CHAR_FORM_FEED = 0X000C; + private static final char CHAR_CARRIAGE_RETURN = 0X000D; + private static final char CHAR_NEXT_LINE = 0X0085; + private static final char CHAR_LINE_SEPARATOR = 0X2028; + private static final char CHAR_PARAGRAPH_SEPARATOR = 0X2029; + private StringUtils() { // This utility class is not publicly instantiable. } @@ -594,4 +602,30 @@ public final class StringUtils { return sb + "]"; } } + + /** + * Returns whether the last composed word contains line-breaking character (e.g. CR or LF). + * @param text the text to be examined. + * @return {@code true} if the last composed word contains line-breaking separator. + */ + @UsedForTesting + public static boolean hasLineBreakCharacter(final String text) { + if (TextUtils.isEmpty(text)) { + return false; + } + for (int i = text.length() - 1; i >= 0; --i) { + final char c = text.charAt(i); + switch (c) { + case CHAR_LINE_FEED: + case CHAR_VERTICAL_TAB: + case CHAR_FORM_FEED: + case CHAR_CARRIAGE_RETURN: + case CHAR_NEXT_LINE: + case CHAR_LINE_SEPARATOR: + case CHAR_PARAGRAPH_SEPARATOR: + return true; + } + } + return false; + } } diff --git a/java/src/com/android/inputmethod/latin/utils/SuggestionResults.java b/java/src/com/android/inputmethod/latin/utils/SuggestionResults.java index eaa5743d4..d6f644228 100644 --- a/java/src/com/android/inputmethod/latin/utils/SuggestionResults.java +++ b/java/src/com/android/inputmethod/latin/utils/SuggestionResults.java @@ -22,7 +22,6 @@ import com.android.inputmethod.latin.define.ProductionFlags; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; -import java.util.Locale; import java.util.TreeSet; /** @@ -31,14 +30,17 @@ import java.util.TreeSet; */ public final class SuggestionResults extends TreeSet<SuggestedWordInfo> { public final ArrayList<SuggestedWordInfo> mRawSuggestions; + // TODO: Instead of a boolean , we may want to include the context of this suggestion results, + // such as {@link PrevWordsInfo}. + public final boolean mIsBeginningOfSentence; private final int mCapacity; - public SuggestionResults(final int capacity) { - this(sSuggestedWordInfoComparator, capacity); + public SuggestionResults(final int capacity, final boolean isBeginningOfSentence) { + this(sSuggestedWordInfoComparator, capacity, isBeginningOfSentence); } - public SuggestionResults(final Comparator<SuggestedWordInfo> comparator, - final int capacity) { + private SuggestionResults(final Comparator<SuggestedWordInfo> comparator, + final int capacity, final boolean isBeginningOfSentence) { super(comparator); mCapacity = capacity; if (ProductionFlags.INCLUDE_RAW_SUGGESTIONS) { @@ -46,6 +48,7 @@ public final class SuggestionResults extends TreeSet<SuggestedWordInfo> { } else { mRawSuggestions = null; } + mIsBeginningOfSentence = isBeginningOfSentence; } @Override |