RichInputConnectionAndTextRangeTests.java revision 4569a734adffe1d12a4a1e8ff751608c1e4b2faf
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, 0); 237 assertTrue(TextUtils.equals("word", r.mWord)); 238 239 // more than one word 240 r = ic.getWordRangeAtCursor(SPACE, 1); 241 assertTrue(TextUtils.equals("word word", r.mWord)); 242 ic.endBatchEdit(); 243 244 // tab character instead of space 245 mockInputMethodService.setInputConnection(new MockConnection("one\tword\two", "rd", et)); 246 ic.beginBatchEdit(); 247 r = ic.getWordRangeAtCursor(TAB, 1); 248 ic.endBatchEdit(); 249 assertTrue(TextUtils.equals("word\tword", r.mWord)); 250 251 // only one word doesn't go too far 252 mockInputMethodService.setInputConnection(new MockConnection("one\tword\two", "rd", et)); 253 ic.beginBatchEdit(); 254 r = ic.getWordRangeAtCursor(TAB, 1); 255 ic.endBatchEdit(); 256 assertTrue(TextUtils.equals("word\tword", r.mWord)); 257 258 // tab or space 259 mockInputMethodService.setInputConnection(new MockConnection("one word\two", "rd", et)); 260 ic.beginBatchEdit(); 261 r = ic.getWordRangeAtCursor(SPACE_TAB, 1); 262 ic.endBatchEdit(); 263 assertTrue(TextUtils.equals("word\tword", r.mWord)); 264 265 // tab or space multiword 266 mockInputMethodService.setInputConnection(new MockConnection("one word\two", "rd", et)); 267 ic.beginBatchEdit(); 268 r = ic.getWordRangeAtCursor(SPACE_TAB, 2); 269 ic.endBatchEdit(); 270 assertTrue(TextUtils.equals("one word\tword", r.mWord)); 271 272 // splitting on supplementary character 273 mockInputMethodService.setInputConnection( 274 new MockConnection("one word" + SUPPLEMENTARY_CHAR + "wo", "rd", et)); 275 ic.beginBatchEdit(); 276 r = ic.getWordRangeAtCursor(StringUtils.toSortedCodePointArray(SUPPLEMENTARY_CHAR), 0); 277 ic.endBatchEdit(); 278 assertTrue(TextUtils.equals("word", r.mWord)); 279 } 280 281 /** 282 * Test logic in getting the word range at the cursor. 283 */ 284 public void testGetSuggestionSpansAtWord() { 285 helpTestGetSuggestionSpansAtWord(10); 286 helpTestGetSuggestionSpansAtWord(12); 287 helpTestGetSuggestionSpansAtWord(15); 288 helpTestGetSuggestionSpansAtWord(16); 289 } 290 291 private void helpTestGetSuggestionSpansAtWord(final int cursorPos) { 292 final MockInputMethodService mockInputMethodService = new MockInputMethodService(); 293 final RichInputConnection ic = new RichInputConnection(mockInputMethodService); 294 295 final String[] SUGGESTIONS1 = { "swing", "strong" }; 296 final String[] SUGGESTIONS2 = { "storing", "strung" }; 297 298 // Test the usual case. 299 SpannableString 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 mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos)); 303 TextRange r; 304 SuggestionSpan[] suggestions; 305 306 r = ic.getWordRangeAtCursor(SPACE, 0); 307 suggestions = r.getSuggestionSpansAtWord(); 308 assertEquals(suggestions.length, 1); 309 MoreAsserts.assertEquals(suggestions[0].getSuggestions(), SUGGESTIONS1); 310 311 // Test the case with 2 suggestion spans in the same place. 312 text = new SpannableString("This is a string for test"); 313 text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS1, 0 /* flags */), 314 10 /* start */, 16 /* end */, 0 /* flags */); 315 text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS2, 0 /* flags */), 316 10 /* start */, 16 /* end */, 0 /* flags */); 317 mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos)); 318 r = ic.getWordRangeAtCursor(SPACE, 0); 319 suggestions = r.getSuggestionSpansAtWord(); 320 assertEquals(suggestions.length, 2); 321 MoreAsserts.assertEquals(suggestions[0].getSuggestions(), SUGGESTIONS1); 322 MoreAsserts.assertEquals(suggestions[1].getSuggestions(), SUGGESTIONS2); 323 324 // Test a case with overlapping spans, 2nd extending past the start of the word 325 text = new SpannableString("This is a string for test"); 326 text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS1, 0 /* flags */), 327 10 /* start */, 16 /* end */, 0 /* flags */); 328 text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS2, 0 /* flags */), 329 5 /* start */, 16 /* end */, 0 /* flags */); 330 mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos)); 331 r = ic.getWordRangeAtCursor(SPACE, 0); 332 suggestions = r.getSuggestionSpansAtWord(); 333 assertEquals(suggestions.length, 1); 334 MoreAsserts.assertEquals(suggestions[0].getSuggestions(), SUGGESTIONS1); 335 336 // Test a case with overlapping spans, 2nd extending past the end of the word 337 text = new SpannableString("This is a string for test"); 338 text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS1, 0 /* flags */), 339 10 /* start */, 16 /* end */, 0 /* flags */); 340 text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS2, 0 /* flags */), 341 10 /* start */, 20 /* end */, 0 /* flags */); 342 mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos)); 343 r = ic.getWordRangeAtCursor(SPACE, 0); 344 suggestions = r.getSuggestionSpansAtWord(); 345 assertEquals(suggestions.length, 1); 346 MoreAsserts.assertEquals(suggestions[0].getSuggestions(), SUGGESTIONS1); 347 348 // Test a case with overlapping spans, 2nd extending past both ends of the word 349 text = new SpannableString("This is a string for test"); 350 text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS1, 0 /* flags */), 351 10 /* start */, 16 /* end */, 0 /* flags */); 352 text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS2, 0 /* flags */), 353 5 /* start */, 20 /* end */, 0 /* flags */); 354 mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos)); 355 r = ic.getWordRangeAtCursor(SPACE, 0); 356 suggestions = r.getSuggestionSpansAtWord(); 357 assertEquals(suggestions.length, 1); 358 MoreAsserts.assertEquals(suggestions[0].getSuggestions(), SUGGESTIONS1); 359 360 // Test a case with overlapping spans, none right on the word 361 text = new SpannableString("This is a string for test"); 362 text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS1, 0 /* flags */), 363 5 /* start */, 16 /* end */, 0 /* flags */); 364 text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS2, 0 /* flags */), 365 5 /* start */, 20 /* end */, 0 /* flags */); 366 mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos)); 367 r = ic.getWordRangeAtCursor(SPACE, 0); 368 suggestions = r.getSuggestionSpansAtWord(); 369 assertEquals(suggestions.length, 0); 370 } 371 372 public void testCursorTouchingWord() { 373 final MockInputMethodService ims = new MockInputMethodService(); 374 final RichInputConnection ic = new RichInputConnection(ims); 375 final SpacingAndPunctuations sap = mSpacingAndPunctuations; 376 377 ims.setInputConnection(new MockConnection("users", 5)); 378 ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); 379 assertTrue(ic.isCursorTouchingWord(sap)); 380 381 ims.setInputConnection(new MockConnection("users'", 5)); 382 ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); 383 assertTrue(ic.isCursorTouchingWord(sap)); 384 385 ims.setInputConnection(new MockConnection("users'", 6)); 386 ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); 387 assertTrue(ic.isCursorTouchingWord(sap)); 388 389 ims.setInputConnection(new MockConnection("'users'", 6)); 390 ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); 391 assertTrue(ic.isCursorTouchingWord(sap)); 392 393 ims.setInputConnection(new MockConnection("'users'", 7)); 394 ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); 395 assertTrue(ic.isCursorTouchingWord(sap)); 396 397 ims.setInputConnection(new MockConnection("users '", 6)); 398 ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); 399 assertFalse(ic.isCursorTouchingWord(sap)); 400 401 ims.setInputConnection(new MockConnection("users '", 7)); 402 ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); 403 assertFalse(ic.isCursorTouchingWord(sap)); 404 405 ims.setInputConnection(new MockConnection("re-", 3)); 406 ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); 407 assertTrue(ic.isCursorTouchingWord(sap)); 408 409 ims.setInputConnection(new MockConnection("re--", 4)); 410 ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); 411 assertFalse(ic.isCursorTouchingWord(sap)); 412 413 ims.setInputConnection(new MockConnection("-", 1)); 414 ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); 415 assertFalse(ic.isCursorTouchingWord(sap)); 416 417 ims.setInputConnection(new MockConnection("--", 2)); 418 ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); 419 assertFalse(ic.isCursorTouchingWord(sap)); 420 421 ims.setInputConnection(new MockConnection(" -", 2)); 422 ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); 423 assertFalse(ic.isCursorTouchingWord(sap)); 424 425 ims.setInputConnection(new MockConnection(" --", 3)); 426 ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); 427 assertFalse(ic.isCursorTouchingWord(sap)); 428 429 ims.setInputConnection(new MockConnection(" users '", 1)); 430 ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); 431 assertTrue(ic.isCursorTouchingWord(sap)); 432 433 ims.setInputConnection(new MockConnection(" users '", 3)); 434 ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); 435 assertTrue(ic.isCursorTouchingWord(sap)); 436 437 ims.setInputConnection(new MockConnection(" users '", 7)); 438 ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); 439 assertFalse(ic.isCursorTouchingWord(sap)); 440 441 ims.setInputConnection(new MockConnection(" users are", 7)); 442 ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); 443 assertTrue(ic.isCursorTouchingWord(sap)); 444 445 ims.setInputConnection(new MockConnection(" users 'are", 7)); 446 ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); 447 assertFalse(ic.isCursorTouchingWord(sap)); 448 } 449} 450