/* * 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; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import android.content.res.Resources; import android.inputmethodservice.InputMethodService; import android.os.Parcel; import android.text.SpannableString; import android.text.TextUtils; import android.text.style.SuggestionSpan; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputConnectionWrapper; import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; import com.android.inputmethod.latin.common.Constants; import com.android.inputmethod.latin.common.StringUtils; import com.android.inputmethod.latin.settings.SpacingAndPunctuations; import com.android.inputmethod.latin.utils.NgramContextUtils; import com.android.inputmethod.latin.utils.RunInLocale; import com.android.inputmethod.latin.utils.ScriptUtils; import com.android.inputmethod.latin.utils.TextRange; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import java.util.Locale; @SmallTest @RunWith(AndroidJUnit4.class) public class RichInputConnectionAndTextRangeTests { // The following is meant to be a reasonable default for // the "word_separators" resource. private SpacingAndPunctuations mSpacingAndPunctuations; @Before public void setUp() throws Exception { final RunInLocale job = new RunInLocale() { @Override protected SpacingAndPunctuations job(final Resources res) { return new SpacingAndPunctuations(res); } }; final Resources res = InstrumentationRegistry.getTargetContext().getResources(); mSpacingAndPunctuations = job.runInLocale(res, Locale.ENGLISH); } private class MockConnection extends InputConnectionWrapper { final CharSequence mTextBefore; final CharSequence mTextAfter; final ExtractedText mExtractedText; public MockConnection(final CharSequence text, final int cursorPosition) { super(null, false); // Interaction of spans with Parcels is completely non-trivial, but in the actual case // the CharSequences do go through Parcels because they go through IPC. There // are some significant differences between the behavior of Spanned objects that // have and that have not gone through parceling, so it's much easier to simulate // the environment with Parcels than try to emulate things by hand. final Parcel p = Parcel.obtain(); TextUtils.writeToParcel(text.subSequence(0, cursorPosition), p, 0 /* flags */); TextUtils.writeToParcel(text.subSequence(cursorPosition, text.length()), p, 0 /* flags */); final byte[] marshalled = p.marshall(); p.unmarshall(marshalled, 0, marshalled.length); p.setDataPosition(0); mTextBefore = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(p); mTextAfter = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(p); mExtractedText = null; p.recycle(); } public MockConnection(String textBefore, String textAfter, ExtractedText extractedText) { super(null, false); mTextBefore = textBefore; mTextAfter = textAfter; mExtractedText = extractedText; } public int cursorPos() { return mTextBefore.length(); } /* (non-Javadoc) * @see android.view.inputmethod.InputConnectionWrapper#getTextBeforeCursor(int, int) */ @Override public CharSequence getTextBeforeCursor(int n, int flags) { return mTextBefore; } /* (non-Javadoc) * @see android.view.inputmethod.InputConnectionWrapper#getTextAfterCursor(int, int) */ @Override public CharSequence getTextAfterCursor(int n, int flags) { return mTextAfter; } /* (non-Javadoc) * @see android.view.inputmethod.InputConnectionWrapper#getExtractedText( * ExtractedTextRequest, int) */ @Override public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) { return mExtractedText; } @Override public boolean beginBatchEdit() { return true; } @Override public boolean endBatchEdit() { return true; } @Override public boolean finishComposingText() { return true; } } static class MockInputMethodService extends InputMethodService { private MockConnection mMockConnection; public void setInputConnection(final MockConnection mockConnection) { mMockConnection = mockConnection; } public int cursorPos() { return mMockConnection.cursorPos(); } @Override public InputConnection getCurrentInputConnection() { return mMockConnection; } } /************************** Tests ************************/ /** * Test for getting previous word (for bigram suggestions) */ @Test public void testGetPreviousWord() { // If one of the following cases breaks, the bigram suggestions won't work. assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord( "abc def", mSpacingAndPunctuations, 2).getNthPrevWord(1), "abc"); assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord( "abc", mSpacingAndPunctuations, 2), NgramContext.BEGINNING_OF_SENTENCE); assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord( "abc. def", mSpacingAndPunctuations, 2), NgramContext.BEGINNING_OF_SENTENCE); assertFalse(NgramContextUtils.getNgramContextFromNthPreviousWord( "abc def", mSpacingAndPunctuations, 2).isBeginningOfSentenceContext()); assertTrue(NgramContextUtils.getNgramContextFromNthPreviousWord( "abc", mSpacingAndPunctuations, 2).isBeginningOfSentenceContext()); // For n-gram assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord( "abc def", mSpacingAndPunctuations, 1).getNthPrevWord(1), "def"); assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord( "abc def", mSpacingAndPunctuations, 1).getNthPrevWord(2), "abc"); assertTrue(NgramContextUtils.getNgramContextFromNthPreviousWord( "abc def", mSpacingAndPunctuations, 2).isNthPrevWordBeginningOfSentence(2)); // The following tests reflect the current behavior of the function // RichInputConnection#getNthPreviousWord. // TODO: However at this time, the code does never go // into such a path, so it should be safe to change the behavior of // this function if needed - especially since it does not seem very // logical. These tests are just there to catch any unintentional // changes in the behavior of the RichInputConnection#getPreviousWord method. assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord( "abc def ", mSpacingAndPunctuations, 2).getNthPrevWord(1), "abc"); assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord( "abc def.", mSpacingAndPunctuations, 2).getNthPrevWord(1), "abc"); assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord( "abc def .", mSpacingAndPunctuations, 2).getNthPrevWord(1), "def"); assertTrue(NgramContextUtils.getNgramContextFromNthPreviousWord( "abc ", mSpacingAndPunctuations, 2).isBeginningOfSentenceContext()); assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord( "abc def", mSpacingAndPunctuations, 1).getNthPrevWord(1), "def"); assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord( "abc def ", mSpacingAndPunctuations, 1).getNthPrevWord(1), "def"); assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord( "abc 'def", mSpacingAndPunctuations, 1).getNthPrevWord(1), "'def"); assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord( "abc def.", mSpacingAndPunctuations, 1), NgramContext.BEGINNING_OF_SENTENCE); assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord( "abc def .", mSpacingAndPunctuations, 1), NgramContext.BEGINNING_OF_SENTENCE); assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord( "abc, def", mSpacingAndPunctuations, 2), NgramContext.EMPTY_PREV_WORDS_INFO); // question mark is treated as the end of the sentence. Hence, beginning of the // sentence is expected. assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord( "abc? def", mSpacingAndPunctuations, 2), NgramContext.BEGINNING_OF_SENTENCE); // Exclamation mark is treated as the end of the sentence. Hence, beginning of the // sentence is expected. assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord( "abc! def", mSpacingAndPunctuations, 2), NgramContext.BEGINNING_OF_SENTENCE); assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord( "abc 'def", mSpacingAndPunctuations, 2), NgramContext.EMPTY_PREV_WORDS_INFO); } @Test public void testGetWordRangeAtCursor() { /** * Test logic in getting the word range at the cursor. */ final SpacingAndPunctuations SPACE = new SpacingAndPunctuations( mSpacingAndPunctuations, new int[] { Constants.CODE_SPACE }); final SpacingAndPunctuations TAB = new SpacingAndPunctuations( mSpacingAndPunctuations, new int[] { Constants.CODE_TAB }); // A character that needs surrogate pair to represent its code point (U+2008A). final String SUPPLEMENTARY_CHAR_STRING = "\uD840\uDC8A"; final SpacingAndPunctuations SUPPLEMENTARY_CHAR = new SpacingAndPunctuations( mSpacingAndPunctuations, StringUtils.toSortedCodePointArray( SUPPLEMENTARY_CHAR_STRING)); final String HIRAGANA_WORD = "\u3042\u3044\u3046\u3048\u304A"; // あいうえお final String GREEK_WORD = "\u03BA\u03B1\u03B9"; // και ExtractedText et = new ExtractedText(); final MockInputMethodService mockInputMethodService = new MockInputMethodService(); final RichInputConnection ic = new RichInputConnection(mockInputMethodService); mockInputMethodService.setInputConnection(new MockConnection("word wo", "rd", et)); et.startOffset = 0; et.selectionStart = 7; TextRange r; ic.beginBatchEdit(); // basic case r = ic.getWordRangeAtCursor(SPACE, ScriptUtils.SCRIPT_LATIN); assertTrue(TextUtils.equals("word", r.mWord)); // tab character instead of space mockInputMethodService.setInputConnection(new MockConnection("one\tword\two", "rd", et)); ic.beginBatchEdit(); r = ic.getWordRangeAtCursor(TAB, ScriptUtils.SCRIPT_LATIN); ic.endBatchEdit(); assertTrue(TextUtils.equals("word", r.mWord)); // splitting on supplementary character mockInputMethodService.setInputConnection( new MockConnection("one word" + SUPPLEMENTARY_CHAR_STRING + "wo", "rd", et)); ic.beginBatchEdit(); r = ic.getWordRangeAtCursor(SUPPLEMENTARY_CHAR, ScriptUtils.SCRIPT_LATIN); ic.endBatchEdit(); assertTrue(TextUtils.equals("word", r.mWord)); // split on chars outside the specified script mockInputMethodService.setInputConnection( new MockConnection(HIRAGANA_WORD + "wo", "rd" + GREEK_WORD, et)); ic.beginBatchEdit(); r = ic.getWordRangeAtCursor(SUPPLEMENTARY_CHAR, ScriptUtils.SCRIPT_LATIN); ic.endBatchEdit(); assertTrue(TextUtils.equals("word", r.mWord)); // likewise for greek mockInputMethodService.setInputConnection( new MockConnection("text" + GREEK_WORD, "text", et)); ic.beginBatchEdit(); r = ic.getWordRangeAtCursor(SUPPLEMENTARY_CHAR, ScriptUtils.SCRIPT_GREEK); ic.endBatchEdit(); assertTrue(TextUtils.equals(GREEK_WORD, r.mWord)); } /** * Test logic in getting the word range at the cursor. */ @Test public void testGetSuggestionSpansAtWord() { helpTestGetSuggestionSpansAtWord(10); helpTestGetSuggestionSpansAtWord(12); helpTestGetSuggestionSpansAtWord(15); helpTestGetSuggestionSpansAtWord(16); } private void helpTestGetSuggestionSpansAtWord(final int cursorPos) { final SpacingAndPunctuations SPACE = new SpacingAndPunctuations( mSpacingAndPunctuations, new int[] { Constants.CODE_SPACE }); final MockInputMethodService mockInputMethodService = new MockInputMethodService(); final RichInputConnection ic = new RichInputConnection(mockInputMethodService); final String[] SUGGESTIONS1 = { "swing", "strong" }; final String[] SUGGESTIONS2 = { "storing", "strung" }; // Test the usual case. SpannableString text = new SpannableString("This is a string for test"); text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS1, 0 /* flags */), 10 /* start */, 16 /* end */, 0 /* flags */); mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos)); TextRange r; SuggestionSpan[] suggestions; r = ic.getWordRangeAtCursor(SPACE, ScriptUtils.SCRIPT_LATIN); suggestions = r.getSuggestionSpansAtWord(); assertEquals(suggestions.length, 1); assertEquals(suggestions[0].getSuggestions(), SUGGESTIONS1); // Test the case with 2 suggestion spans in the same place. text = new SpannableString("This is a string for test"); text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS1, 0 /* flags */), 10 /* start */, 16 /* end */, 0 /* flags */); text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS2, 0 /* flags */), 10 /* start */, 16 /* end */, 0 /* flags */); mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos)); r = ic.getWordRangeAtCursor(SPACE, ScriptUtils.SCRIPT_LATIN); suggestions = r.getSuggestionSpansAtWord(); assertEquals(suggestions.length, 2); assertEquals(suggestions[0].getSuggestions(), SUGGESTIONS1); assertEquals(suggestions[1].getSuggestions(), SUGGESTIONS2); // Test a case with overlapping spans, 2nd extending past the start of the word text = new SpannableString("This is a string for test"); text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS1, 0 /* flags */), 10 /* start */, 16 /* end */, 0 /* flags */); text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS2, 0 /* flags */), 5 /* start */, 16 /* end */, 0 /* flags */); mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos)); r = ic.getWordRangeAtCursor(SPACE, ScriptUtils.SCRIPT_LATIN); suggestions = r.getSuggestionSpansAtWord(); assertEquals(suggestions.length, 1); assertEquals(suggestions[0].getSuggestions(), SUGGESTIONS1); // Test a case with overlapping spans, 2nd extending past the end of the word text = new SpannableString("This is a string for test"); text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS1, 0 /* flags */), 10 /* start */, 16 /* end */, 0 /* flags */); text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS2, 0 /* flags */), 10 /* start */, 20 /* end */, 0 /* flags */); mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos)); r = ic.getWordRangeAtCursor(SPACE, ScriptUtils.SCRIPT_LATIN); suggestions = r.getSuggestionSpansAtWord(); assertEquals(suggestions.length, 1); assertEquals(suggestions[0].getSuggestions(), SUGGESTIONS1); // Test a case with overlapping spans, 2nd extending past both ends of the word text = new SpannableString("This is a string for test"); text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS1, 0 /* flags */), 10 /* start */, 16 /* end */, 0 /* flags */); text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS2, 0 /* flags */), 5 /* start */, 20 /* end */, 0 /* flags */); mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos)); r = ic.getWordRangeAtCursor(SPACE, ScriptUtils.SCRIPT_LATIN); suggestions = r.getSuggestionSpansAtWord(); assertEquals(suggestions.length, 1); assertEquals(suggestions[0].getSuggestions(), SUGGESTIONS1); // Test a case with overlapping spans, none right on the word text = new SpannableString("This is a string for test"); text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS1, 0 /* flags */), 5 /* start */, 16 /* end */, 0 /* flags */); text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS2, 0 /* flags */), 5 /* start */, 20 /* end */, 0 /* flags */); mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos)); r = ic.getWordRangeAtCursor(SPACE, ScriptUtils.SCRIPT_LATIN); suggestions = r.getSuggestionSpansAtWord(); assertEquals(suggestions.length, 0); } @Test public void testCursorTouchingWord() { final MockInputMethodService ims = new MockInputMethodService(); final RichInputConnection ic = new RichInputConnection(ims); final SpacingAndPunctuations sap = mSpacingAndPunctuations; ims.setInputConnection(new MockConnection("users", 5)); ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); assertTrue(ic.isCursorTouchingWord(sap, true /* checkTextAfter */)); ims.setInputConnection(new MockConnection("users'", 5)); ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); assertTrue(ic.isCursorTouchingWord(sap, true)); ims.setInputConnection(new MockConnection("users'", 6)); ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); assertTrue(ic.isCursorTouchingWord(sap, true)); ims.setInputConnection(new MockConnection("'users'", 6)); ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); assertTrue(ic.isCursorTouchingWord(sap, true)); ims.setInputConnection(new MockConnection("'users'", 7)); ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); assertTrue(ic.isCursorTouchingWord(sap, true)); ims.setInputConnection(new MockConnection("users '", 6)); ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); assertFalse(ic.isCursorTouchingWord(sap, true)); ims.setInputConnection(new MockConnection("users '", 7)); ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); assertFalse(ic.isCursorTouchingWord(sap, true)); ims.setInputConnection(new MockConnection("re-", 3)); ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); assertTrue(ic.isCursorTouchingWord(sap, true)); ims.setInputConnection(new MockConnection("re--", 4)); ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); assertFalse(ic.isCursorTouchingWord(sap, true)); ims.setInputConnection(new MockConnection("-", 1)); ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); assertFalse(ic.isCursorTouchingWord(sap, true)); ims.setInputConnection(new MockConnection("--", 2)); ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); assertFalse(ic.isCursorTouchingWord(sap, true)); ims.setInputConnection(new MockConnection(" -", 2)); ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); assertFalse(ic.isCursorTouchingWord(sap, true)); ims.setInputConnection(new MockConnection(" --", 3)); ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); assertFalse(ic.isCursorTouchingWord(sap, true)); ims.setInputConnection(new MockConnection(" users '", 1)); ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); assertTrue(ic.isCursorTouchingWord(sap, true)); ims.setInputConnection(new MockConnection(" users '", 3)); ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); assertTrue(ic.isCursorTouchingWord(sap, true)); ims.setInputConnection(new MockConnection(" users '", 7)); ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); assertFalse(ic.isCursorTouchingWord(sap, true)); ims.setInputConnection(new MockConnection(" users are", 7)); ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); assertTrue(ic.isCursorTouchingWord(sap, true)); ims.setInputConnection(new MockConnection(" users 'are", 7)); ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); assertFalse(ic.isCursorTouchingWord(sap, true)); } }