RichInputConnectionAndTextRangeTests.java revision 20c89b1cf5e30026844922d312163ffcd1c20b26
1/* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.inputmethod.latin; 18 19import android.content.res.Resources; 20import android.inputmethodservice.InputMethodService; 21import android.os.Parcel; 22import android.test.AndroidTestCase; 23import android.test.MoreAsserts; 24import android.test.suitebuilder.annotation.SmallTest; 25import android.text.SpannableString; 26import android.text.TextUtils; 27import android.text.style.SuggestionSpan; 28import android.view.inputmethod.ExtractedText; 29import android.view.inputmethod.ExtractedTextRequest; 30import android.view.inputmethod.InputConnection; 31import android.view.inputmethod.InputConnectionWrapper; 32 33import com.android.inputmethod.latin.PrevWordsInfo.WordInfo; 34import com.android.inputmethod.latin.settings.SpacingAndPunctuations; 35import com.android.inputmethod.latin.utils.RunInLocale; 36import com.android.inputmethod.latin.utils.StringUtils; 37import com.android.inputmethod.latin.utils.TextRange; 38 39import java.util.Locale; 40 41@SmallTest 42public class RichInputConnectionAndTextRangeTests extends AndroidTestCase { 43 44 // The following is meant to be a reasonable default for 45 // the "word_separators" resource. 46 private SpacingAndPunctuations mSpacingAndPunctuations; 47 48 @Override 49 protected void setUp() throws Exception { 50 super.setUp(); 51 final RunInLocale<SpacingAndPunctuations> job = new RunInLocale<SpacingAndPunctuations>() { 52 @Override 53 protected SpacingAndPunctuations job(final Resources res) { 54 return new SpacingAndPunctuations(res); 55 } 56 }; 57 final Resources res = getContext().getResources(); 58 mSpacingAndPunctuations = job.runInLocale(res, Locale.ENGLISH); 59 } 60 61 private class MockConnection extends InputConnectionWrapper { 62 final CharSequence mTextBefore; 63 final CharSequence mTextAfter; 64 final ExtractedText mExtractedText; 65 66 public MockConnection(final CharSequence text, final int cursorPosition) { 67 super(null, false); 68 // Interaction of spans with Parcels is completely non-trivial, but in the actual case 69 // the CharSequences do go through Parcels because they go through IPC. There 70 // are some significant differences between the behavior of Spanned objects that 71 // have and that have not gone through parceling, so it's much easier to simulate 72 // the environment with Parcels than try to emulate things by hand. 73 final Parcel p = Parcel.obtain(); 74 TextUtils.writeToParcel(text.subSequence(0, cursorPosition), p, 0 /* flags */); 75 TextUtils.writeToParcel(text.subSequence(cursorPosition, text.length()), p, 76 0 /* flags */); 77 final byte[] marshalled = p.marshall(); 78 p.unmarshall(marshalled, 0, marshalled.length); 79 p.setDataPosition(0); 80 mTextBefore = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(p); 81 mTextAfter = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(p); 82 mExtractedText = null; 83 p.recycle(); 84 } 85 86 public MockConnection(String textBefore, String textAfter, ExtractedText extractedText) { 87 super(null, false); 88 mTextBefore = textBefore; 89 mTextAfter = textAfter; 90 mExtractedText = extractedText; 91 } 92 93 public int cursorPos() { 94 return mTextBefore.length(); 95 } 96 97 /* (non-Javadoc) 98 * @see android.view.inputmethod.InputConnectionWrapper#getTextBeforeCursor(int, int) 99 */ 100 @Override 101 public CharSequence getTextBeforeCursor(int n, int flags) { 102 return mTextBefore; 103 } 104 105 /* (non-Javadoc) 106 * @see android.view.inputmethod.InputConnectionWrapper#getTextAfterCursor(int, int) 107 */ 108 @Override 109 public CharSequence getTextAfterCursor(int n, int flags) { 110 return mTextAfter; 111 } 112 113 /* (non-Javadoc) 114 * @see android.view.inputmethod.InputConnectionWrapper#getExtractedText( 115 * ExtractedTextRequest, int) 116 */ 117 @Override 118 public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) { 119 return mExtractedText; 120 } 121 122 @Override 123 public boolean beginBatchEdit() { 124 return true; 125 } 126 127 @Override 128 public boolean endBatchEdit() { 129 return true; 130 } 131 132 @Override 133 public boolean finishComposingText() { 134 return true; 135 } 136 } 137 138 private class MockInputMethodService extends InputMethodService { 139 private MockConnection mMockConnection; 140 public void setInputConnection(final MockConnection mockConnection) { 141 mMockConnection = mockConnection; 142 } 143 public int cursorPos() { 144 return mMockConnection.cursorPos(); 145 } 146 @Override 147 public InputConnection getCurrentInputConnection() { 148 return mMockConnection; 149 } 150 } 151 152 /************************** Tests ************************/ 153 154 /** 155 * Test for getting previous word (for bigram suggestions) 156 */ 157 public void testGetPreviousWord() { 158 // If one of the following cases breaks, the bigram suggestions won't work. 159 assertEquals(RichInputConnection.getPrevWordsInfoFromNthPreviousWord( 160 "abc def", mSpacingAndPunctuations, 2).mPrevWordsInfo[0].mWord, "abc"); 161 assertEquals(RichInputConnection.getPrevWordsInfoFromNthPreviousWord( 162 "abc", mSpacingAndPunctuations, 2), PrevWordsInfo.BEGINNING_OF_SENTENCE); 163 assertEquals(RichInputConnection.getPrevWordsInfoFromNthPreviousWord( 164 "abc. def", mSpacingAndPunctuations, 2), PrevWordsInfo.BEGINNING_OF_SENTENCE); 165 166 assertFalse(RichInputConnection.getPrevWordsInfoFromNthPreviousWord( 167 "abc def", mSpacingAndPunctuations, 2).mPrevWordsInfo[0].mIsBeginningOfSentence); 168 assertTrue(RichInputConnection.getPrevWordsInfoFromNthPreviousWord( 169 "abc", mSpacingAndPunctuations, 2).mPrevWordsInfo[0].mIsBeginningOfSentence); 170 171 // For n-gram 172 assertEquals(RichInputConnection.getPrevWordsInfoFromNthPreviousWord( 173 "abc def", mSpacingAndPunctuations, 1).mPrevWordsInfo[0].mWord, "def"); 174 assertEquals(RichInputConnection.getPrevWordsInfoFromNthPreviousWord( 175 "abc def", mSpacingAndPunctuations, 1).mPrevWordsInfo[1].mWord, "abc"); 176 assertEquals(RichInputConnection.getPrevWordsInfoFromNthPreviousWord( 177 "abc def", mSpacingAndPunctuations, 2).mPrevWordsInfo[1], 178 WordInfo.BEGINNING_OF_SENTENCE); 179 180 // The following tests reflect the current behavior of the function 181 // RichInputConnection#getNthPreviousWord. 182 // TODO: However at this time, the code does never go 183 // into such a path, so it should be safe to change the behavior of 184 // this function if needed - especially since it does not seem very 185 // logical. These tests are just there to catch any unintentional 186 // changes in the behavior of the RichInputConnection#getPreviousWord method. 187 assertEquals(RichInputConnection.getPrevWordsInfoFromNthPreviousWord( 188 "abc def ", mSpacingAndPunctuations, 2).mPrevWordsInfo[0].mWord, "abc"); 189 assertEquals(RichInputConnection.getPrevWordsInfoFromNthPreviousWord( 190 "abc def.", mSpacingAndPunctuations, 2).mPrevWordsInfo[0].mWord, "abc"); 191 assertEquals(RichInputConnection.getPrevWordsInfoFromNthPreviousWord( 192 "abc def .", mSpacingAndPunctuations, 2).mPrevWordsInfo[0].mWord, "def"); 193 assertEquals(RichInputConnection.getPrevWordsInfoFromNthPreviousWord( 194 "abc ", mSpacingAndPunctuations, 2), PrevWordsInfo.BEGINNING_OF_SENTENCE); 195 196 assertEquals(RichInputConnection.getPrevWordsInfoFromNthPreviousWord( 197 "abc def", mSpacingAndPunctuations, 1).mPrevWordsInfo[0].mWord, "def"); 198 assertEquals(RichInputConnection.getPrevWordsInfoFromNthPreviousWord( 199 "abc def ", mSpacingAndPunctuations, 1).mPrevWordsInfo[0].mWord, "def"); 200 assertEquals(RichInputConnection.getPrevWordsInfoFromNthPreviousWord( 201 "abc 'def", mSpacingAndPunctuations, 1).mPrevWordsInfo[0].mWord, "'def"); 202 assertEquals(RichInputConnection.getPrevWordsInfoFromNthPreviousWord( 203 "abc def.", mSpacingAndPunctuations, 1), PrevWordsInfo.BEGINNING_OF_SENTENCE); 204 assertEquals(RichInputConnection.getPrevWordsInfoFromNthPreviousWord( 205 "abc def .", mSpacingAndPunctuations, 1), PrevWordsInfo.BEGINNING_OF_SENTENCE); 206 assertEquals(RichInputConnection.getPrevWordsInfoFromNthPreviousWord( 207 "abc, def", mSpacingAndPunctuations, 2), PrevWordsInfo.EMPTY_PREV_WORDS_INFO); 208 assertEquals(RichInputConnection.getPrevWordsInfoFromNthPreviousWord( 209 "abc? def", mSpacingAndPunctuations, 2), PrevWordsInfo.EMPTY_PREV_WORDS_INFO); 210 assertEquals(RichInputConnection.getPrevWordsInfoFromNthPreviousWord( 211 "abc! def", mSpacingAndPunctuations, 2), PrevWordsInfo.EMPTY_PREV_WORDS_INFO); 212 assertEquals(RichInputConnection.getPrevWordsInfoFromNthPreviousWord( 213 "abc 'def", mSpacingAndPunctuations, 2), PrevWordsInfo.EMPTY_PREV_WORDS_INFO); 214 } 215 216 /** 217 * Test logic in getting the word range at the cursor. 218 */ 219 private static final int[] SPACE = { Constants.CODE_SPACE }; 220 static final int[] TAB = { Constants.CODE_TAB }; 221 private static final int[] SPACE_TAB = StringUtils.toSortedCodePointArray(" \t"); 222 // A character that needs surrogate pair to represent its code point (U+2008A). 223 private static final String SUPPLEMENTARY_CHAR = "\uD840\uDC8A"; 224 225 public void testGetWordRangeAtCursor() { 226 ExtractedText et = new ExtractedText(); 227 final MockInputMethodService mockInputMethodService = new MockInputMethodService(); 228 final RichInputConnection ic = new RichInputConnection(mockInputMethodService); 229 mockInputMethodService.setInputConnection(new MockConnection("word wo", "rd", et)); 230 et.startOffset = 0; 231 et.selectionStart = 7; 232 TextRange r; 233 234 ic.beginBatchEdit(); 235 // basic case 236 r = ic.getWordRangeAtCursor(SPACE); 237 assertTrue(TextUtils.equals("word", r.mWord)); 238 239 // tab character instead of space 240 mockInputMethodService.setInputConnection(new MockConnection("one\tword\two", "rd", et)); 241 ic.beginBatchEdit(); 242 r = ic.getWordRangeAtCursor(TAB); 243 ic.endBatchEdit(); 244 assertTrue(TextUtils.equals("word", r.mWord)); 245 246 // splitting on supplementary character 247 mockInputMethodService.setInputConnection( 248 new MockConnection("one word" + SUPPLEMENTARY_CHAR + "wo", "rd", et)); 249 ic.beginBatchEdit(); 250 r = ic.getWordRangeAtCursor(StringUtils.toSortedCodePointArray(SUPPLEMENTARY_CHAR)); 251 ic.endBatchEdit(); 252 assertTrue(TextUtils.equals("word", r.mWord)); 253 } 254 255 /** 256 * Test logic in getting the word range at the cursor. 257 */ 258 public void testGetSuggestionSpansAtWord() { 259 helpTestGetSuggestionSpansAtWord(10); 260 helpTestGetSuggestionSpansAtWord(12); 261 helpTestGetSuggestionSpansAtWord(15); 262 helpTestGetSuggestionSpansAtWord(16); 263 } 264 265 private void helpTestGetSuggestionSpansAtWord(final int cursorPos) { 266 final MockInputMethodService mockInputMethodService = new MockInputMethodService(); 267 final RichInputConnection ic = new RichInputConnection(mockInputMethodService); 268 269 final String[] SUGGESTIONS1 = { "swing", "strong" }; 270 final String[] SUGGESTIONS2 = { "storing", "strung" }; 271 272 // Test the usual case. 273 SpannableString text = new SpannableString("This is a string for test"); 274 text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS1, 0 /* flags */), 275 10 /* start */, 16 /* end */, 0 /* flags */); 276 mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos)); 277 TextRange r; 278 SuggestionSpan[] suggestions; 279 280 r = ic.getWordRangeAtCursor(SPACE); 281 suggestions = r.getSuggestionSpansAtWord(); 282 assertEquals(suggestions.length, 1); 283 MoreAsserts.assertEquals(suggestions[0].getSuggestions(), SUGGESTIONS1); 284 285 // Test the case with 2 suggestion spans in the same place. 286 text = new SpannableString("This is a string for test"); 287 text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS1, 0 /* flags */), 288 10 /* start */, 16 /* end */, 0 /* flags */); 289 text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS2, 0 /* flags */), 290 10 /* start */, 16 /* end */, 0 /* flags */); 291 mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos)); 292 r = ic.getWordRangeAtCursor(SPACE); 293 suggestions = r.getSuggestionSpansAtWord(); 294 assertEquals(suggestions.length, 2); 295 MoreAsserts.assertEquals(suggestions[0].getSuggestions(), SUGGESTIONS1); 296 MoreAsserts.assertEquals(suggestions[1].getSuggestions(), SUGGESTIONS2); 297 298 // Test a case with overlapping spans, 2nd extending past the start of the word 299 text = new SpannableString("This is a string for test"); 300 text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS1, 0 /* flags */), 301 10 /* start */, 16 /* end */, 0 /* flags */); 302 text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS2, 0 /* flags */), 303 5 /* start */, 16 /* end */, 0 /* flags */); 304 mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos)); 305 r = ic.getWordRangeAtCursor(SPACE); 306 suggestions = r.getSuggestionSpansAtWord(); 307 assertEquals(suggestions.length, 1); 308 MoreAsserts.assertEquals(suggestions[0].getSuggestions(), SUGGESTIONS1); 309 310 // Test a case with overlapping spans, 2nd extending past the end of the word 311 text = new SpannableString("This is a string for test"); 312 text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS1, 0 /* flags */), 313 10 /* start */, 16 /* end */, 0 /* flags */); 314 text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS2, 0 /* flags */), 315 10 /* start */, 20 /* end */, 0 /* flags */); 316 mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos)); 317 r = ic.getWordRangeAtCursor(SPACE); 318 suggestions = r.getSuggestionSpansAtWord(); 319 assertEquals(suggestions.length, 1); 320 MoreAsserts.assertEquals(suggestions[0].getSuggestions(), SUGGESTIONS1); 321 322 // Test a case with overlapping spans, 2nd extending past both ends of the word 323 text = new SpannableString("This is a string for test"); 324 text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS1, 0 /* flags */), 325 10 /* start */, 16 /* end */, 0 /* flags */); 326 text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS2, 0 /* flags */), 327 5 /* start */, 20 /* end */, 0 /* flags */); 328 mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos)); 329 r = ic.getWordRangeAtCursor(SPACE); 330 suggestions = r.getSuggestionSpansAtWord(); 331 assertEquals(suggestions.length, 1); 332 MoreAsserts.assertEquals(suggestions[0].getSuggestions(), SUGGESTIONS1); 333 334 // Test a case with overlapping spans, none right on the word 335 text = new SpannableString("This is a string for test"); 336 text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS1, 0 /* flags */), 337 5 /* start */, 16 /* end */, 0 /* flags */); 338 text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS2, 0 /* flags */), 339 5 /* start */, 20 /* end */, 0 /* flags */); 340 mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos)); 341 r = ic.getWordRangeAtCursor(SPACE); 342 suggestions = r.getSuggestionSpansAtWord(); 343 assertEquals(suggestions.length, 0); 344 } 345 346 public void testCursorTouchingWord() { 347 final MockInputMethodService ims = new MockInputMethodService(); 348 final RichInputConnection ic = new RichInputConnection(ims); 349 final SpacingAndPunctuations sap = mSpacingAndPunctuations; 350 351 ims.setInputConnection(new MockConnection("users", 5)); 352 ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); 353 assertTrue(ic.isCursorTouchingWord(sap)); 354 355 ims.setInputConnection(new MockConnection("users'", 5)); 356 ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); 357 assertTrue(ic.isCursorTouchingWord(sap)); 358 359 ims.setInputConnection(new MockConnection("users'", 6)); 360 ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); 361 assertTrue(ic.isCursorTouchingWord(sap)); 362 363 ims.setInputConnection(new MockConnection("'users'", 6)); 364 ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); 365 assertTrue(ic.isCursorTouchingWord(sap)); 366 367 ims.setInputConnection(new MockConnection("'users'", 7)); 368 ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); 369 assertTrue(ic.isCursorTouchingWord(sap)); 370 371 ims.setInputConnection(new MockConnection("users '", 6)); 372 ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); 373 assertFalse(ic.isCursorTouchingWord(sap)); 374 375 ims.setInputConnection(new MockConnection("users '", 7)); 376 ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); 377 assertFalse(ic.isCursorTouchingWord(sap)); 378 379 ims.setInputConnection(new MockConnection("re-", 3)); 380 ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); 381 assertTrue(ic.isCursorTouchingWord(sap)); 382 383 ims.setInputConnection(new MockConnection("re--", 4)); 384 ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); 385 assertFalse(ic.isCursorTouchingWord(sap)); 386 387 ims.setInputConnection(new MockConnection("-", 1)); 388 ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); 389 assertFalse(ic.isCursorTouchingWord(sap)); 390 391 ims.setInputConnection(new MockConnection("--", 2)); 392 ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); 393 assertFalse(ic.isCursorTouchingWord(sap)); 394 395 ims.setInputConnection(new MockConnection(" -", 2)); 396 ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); 397 assertFalse(ic.isCursorTouchingWord(sap)); 398 399 ims.setInputConnection(new MockConnection(" --", 3)); 400 ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); 401 assertFalse(ic.isCursorTouchingWord(sap)); 402 403 ims.setInputConnection(new MockConnection(" users '", 1)); 404 ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); 405 assertTrue(ic.isCursorTouchingWord(sap)); 406 407 ims.setInputConnection(new MockConnection(" users '", 3)); 408 ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); 409 assertTrue(ic.isCursorTouchingWord(sap)); 410 411 ims.setInputConnection(new MockConnection(" users '", 7)); 412 ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); 413 assertFalse(ic.isCursorTouchingWord(sap)); 414 415 ims.setInputConnection(new MockConnection(" users are", 7)); 416 ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); 417 assertTrue(ic.isCursorTouchingWord(sap)); 418 419 ims.setInputConnection(new MockConnection(" users 'are", 7)); 420 ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); 421 assertFalse(ic.isCursorTouchingWord(sap)); 422 } 423} 424