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