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 17 package com.android.inputmethod.latin; 18 19 import com.android.inputmethod.latin.utils.TextRange; 20 21 import android.inputmethodservice.InputMethodService; 22 import android.os.Parcel; 23 import android.test.AndroidTestCase; 24 import android.test.MoreAsserts; 25 import android.test.suitebuilder.annotation.SmallTest; 26 import android.text.SpannableString; 27 import android.text.Spanned; 28 import android.text.TextUtils; 29 import android.text.style.SuggestionSpan; 30 import android.view.inputmethod.ExtractedText; 31 import android.view.inputmethod.ExtractedTextRequest; 32 import android.view.inputmethod.InputConnection; 33 import android.view.inputmethod.InputConnectionWrapper; 34 35 import java.util.Locale; 36 37 @SmallTest 38 public class RichInputConnectionAndTextRangeTests extends AndroidTestCase { 39 40 // The following is meant to be a reasonable default for 41 // the "word_separators" resource. 42 private static final String sSeparators = ".,:;!?-"; 43 44 @Override 45 protected void setUp() throws Exception { 46 super.setUp(); 47 } 48 49 private class MockConnection extends InputConnectionWrapper { 50 final CharSequence mTextBefore; 51 final CharSequence mTextAfter; 52 final ExtractedText mExtractedText; 53 54 public MockConnection(final CharSequence text, final int cursorPosition) { 55 super(null, false); 56 // Interaction of spans with Parcels is completely non-trivial, but in the actual case 57 // the CharSequences do go through Parcels because they go through IPC. There 58 // are some significant differences between the behavior of Spanned objects that 59 // have and that have not gone through parceling, so it's much easier to simulate 60 // the environment with Parcels than try to emulate things by hand. 61 final Parcel p = Parcel.obtain(); 62 TextUtils.writeToParcel(text.subSequence(0, cursorPosition), p, 0 /* flags */); 63 TextUtils.writeToParcel(text.subSequence(cursorPosition, text.length()), p, 64 0 /* flags */); 65 final byte[] marshalled = p.marshall(); 66 p.unmarshall(marshalled, 0, marshalled.length); 67 p.setDataPosition(0); 68 mTextBefore = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(p); 69 mTextAfter = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(p); 70 mExtractedText = null; 71 p.recycle(); 72 } 73 74 public MockConnection(String textBefore, String textAfter, ExtractedText extractedText) { 75 super(null, false); 76 mTextBefore = textBefore; 77 mTextAfter = textAfter; 78 mExtractedText = extractedText; 79 } 80 81 /* (non-Javadoc) 82 * @see android.view.inputmethod.InputConnectionWrapper#getTextBeforeCursor(int, int) 83 */ 84 @Override 85 public CharSequence getTextBeforeCursor(int n, int flags) { 86 return mTextBefore; 87 } 88 89 /* (non-Javadoc) 90 * @see android.view.inputmethod.InputConnectionWrapper#getTextAfterCursor(int, int) 91 */ 92 @Override 93 public CharSequence getTextAfterCursor(int n, int flags) { 94 return mTextAfter; 95 } 96 97 /* (non-Javadoc) 98 * @see android.view.inputmethod.InputConnectionWrapper#getExtractedText( 99 * ExtractedTextRequest, int) 100 */ 101 @Override 102 public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) { 103 return mExtractedText; 104 } 105 106 @Override 107 public boolean beginBatchEdit() { 108 return true; 109 } 110 111 @Override 112 public boolean endBatchEdit() { 113 return true; 114 } 115 116 @Override 117 public boolean finishComposingText() { 118 return true; 119 } 120 } 121 122 private class MockInputMethodService extends InputMethodService { 123 InputConnection mInputConnection; 124 public void setInputConnection(final InputConnection inputConnection) { 125 mInputConnection = inputConnection; 126 } 127 @Override 128 public InputConnection getCurrentInputConnection() { 129 return mInputConnection; 130 } 131 } 132 133 /************************** Tests ************************/ 134 135 /** 136 * Test for getting previous word (for bigram suggestions) 137 */ 138 public void testGetPreviousWord() { 139 // If one of the following cases breaks, the bigram suggestions won't work. 140 assertEquals(RichInputConnection.getNthPreviousWord("abc def", sSeparators, 2), "abc"); 141 assertNull(RichInputConnection.getNthPreviousWord("abc", sSeparators, 2)); 142 assertNull(RichInputConnection.getNthPreviousWord("abc. def", sSeparators, 2)); 143 144 // The following tests reflect the current behavior of the function 145 // RichInputConnection#getNthPreviousWord. 146 // TODO: However at this time, the code does never go 147 // into such a path, so it should be safe to change the behavior of 148 // this function if needed - especially since it does not seem very 149 // logical. These tests are just there to catch any unintentional 150 // changes in the behavior of the RichInputConnection#getPreviousWord method. 151 assertEquals(RichInputConnection.getNthPreviousWord("abc def ", sSeparators, 2), "abc"); 152 assertEquals(RichInputConnection.getNthPreviousWord("abc def.", sSeparators, 2), "abc"); 153 assertEquals(RichInputConnection.getNthPreviousWord("abc def .", sSeparators, 2), "def"); 154 assertNull(RichInputConnection.getNthPreviousWord("abc ", sSeparators, 2)); 155 156 assertEquals(RichInputConnection.getNthPreviousWord("abc def", sSeparators, 1), "def"); 157 assertEquals(RichInputConnection.getNthPreviousWord("abc def ", sSeparators, 1), "def"); 158 assertNull(RichInputConnection.getNthPreviousWord("abc def.", sSeparators, 1)); 159 assertNull(RichInputConnection.getNthPreviousWord("abc def .", sSeparators, 1)); 160 } 161 162 /** 163 * Test logic in getting the word range at the cursor. 164 */ 165 public void testGetWordRangeAtCursor() { 166 ExtractedText et = new ExtractedText(); 167 final MockInputMethodService mockInputMethodService = new MockInputMethodService(); 168 final RichInputConnection ic = new RichInputConnection(mockInputMethodService); 169 mockInputMethodService.setInputConnection(new MockConnection("word wo", "rd", et)); 170 et.startOffset = 0; 171 et.selectionStart = 7; 172 TextRange r; 173 174 ic.beginBatchEdit(); 175 // basic case 176 r = ic.getWordRangeAtCursor(" ", 0); 177 assertTrue(TextUtils.equals("word", r.mWord)); 178 179 // more than one word 180 r = ic.getWordRangeAtCursor(" ", 1); 181 assertTrue(TextUtils.equals("word word", r.mWord)); 182 ic.endBatchEdit(); 183 184 // tab character instead of space 185 mockInputMethodService.setInputConnection(new MockConnection("one\tword\two", "rd", et)); 186 ic.beginBatchEdit(); 187 r = ic.getWordRangeAtCursor("\t", 1); 188 ic.endBatchEdit(); 189 assertTrue(TextUtils.equals("word\tword", r.mWord)); 190 191 // only one word doesn't go too far 192 mockInputMethodService.setInputConnection(new MockConnection("one\tword\two", "rd", et)); 193 ic.beginBatchEdit(); 194 r = ic.getWordRangeAtCursor("\t", 1); 195 ic.endBatchEdit(); 196 assertTrue(TextUtils.equals("word\tword", r.mWord)); 197 198 // tab or space 199 mockInputMethodService.setInputConnection(new MockConnection("one word\two", "rd", et)); 200 ic.beginBatchEdit(); 201 r = ic.getWordRangeAtCursor(" \t", 1); 202 ic.endBatchEdit(); 203 assertTrue(TextUtils.equals("word\tword", r.mWord)); 204 205 // tab or space multiword 206 mockInputMethodService.setInputConnection(new MockConnection("one word\two", "rd", et)); 207 ic.beginBatchEdit(); 208 r = ic.getWordRangeAtCursor(" \t", 2); 209 ic.endBatchEdit(); 210 assertTrue(TextUtils.equals("one word\tword", r.mWord)); 211 212 // splitting on supplementary character 213 final String supplementaryChar = "\uD840\uDC8A"; 214 mockInputMethodService.setInputConnection( 215 new MockConnection("one word" + supplementaryChar + "wo", "rd", et)); 216 ic.beginBatchEdit(); 217 r = ic.getWordRangeAtCursor(supplementaryChar, 0); 218 ic.endBatchEdit(); 219 assertTrue(TextUtils.equals("word", r.mWord)); 220 } 221 222 /** 223 * Test logic in getting the word range at the cursor. 224 */ 225 public void testGetSuggestionSpansAtWord() { 226 helpTestGetSuggestionSpansAtWord(10); 227 helpTestGetSuggestionSpansAtWord(12); 228 helpTestGetSuggestionSpansAtWord(15); 229 helpTestGetSuggestionSpansAtWord(16); 230 } 231 232 private void helpTestGetSuggestionSpansAtWord(final int cursorPos) { 233 final MockInputMethodService mockInputMethodService = new MockInputMethodService(); 234 final RichInputConnection ic = new RichInputConnection(mockInputMethodService); 235 236 final String[] SUGGESTIONS1 = { "swing", "strong" }; 237 final String[] SUGGESTIONS2 = { "storing", "strung" }; 238 239 // Test the usual case. 240 SpannableString text = new SpannableString("This is a string for test"); 241 text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS1, 0 /* flags */), 242 10 /* start */, 16 /* end */, 0 /* flags */); 243 mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos)); 244 TextRange r; 245 SuggestionSpan[] suggestions; 246 247 r = ic.getWordRangeAtCursor(" ", 0); 248 suggestions = r.getSuggestionSpansAtWord(); 249 assertEquals(suggestions.length, 1); 250 MoreAsserts.assertEquals(suggestions[0].getSuggestions(), SUGGESTIONS1); 251 252 // Test the case with 2 suggestion spans in the same place. 253 text = new SpannableString("This is a string for test"); 254 text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS1, 0 /* flags */), 255 10 /* start */, 16 /* end */, 0 /* flags */); 256 text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS2, 0 /* flags */), 257 10 /* start */, 16 /* end */, 0 /* flags */); 258 mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos)); 259 r = ic.getWordRangeAtCursor(" ", 0); 260 suggestions = r.getSuggestionSpansAtWord(); 261 assertEquals(suggestions.length, 2); 262 MoreAsserts.assertEquals(suggestions[0].getSuggestions(), SUGGESTIONS1); 263 MoreAsserts.assertEquals(suggestions[1].getSuggestions(), SUGGESTIONS2); 264 265 // Test a case with overlapping spans, 2nd extending past the start of the word 266 text = new SpannableString("This is a string for test"); 267 text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS1, 0 /* flags */), 268 10 /* start */, 16 /* end */, 0 /* flags */); 269 text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS2, 0 /* flags */), 270 5 /* start */, 16 /* end */, 0 /* flags */); 271 mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos)); 272 r = ic.getWordRangeAtCursor(" ", 0); 273 suggestions = r.getSuggestionSpansAtWord(); 274 assertEquals(suggestions.length, 1); 275 MoreAsserts.assertEquals(suggestions[0].getSuggestions(), SUGGESTIONS1); 276 277 // Test a case with overlapping spans, 2nd extending past the end of the word 278 text = new SpannableString("This is a string for test"); 279 text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS1, 0 /* flags */), 280 10 /* start */, 16 /* end */, 0 /* flags */); 281 text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS2, 0 /* flags */), 282 10 /* start */, 20 /* end */, 0 /* flags */); 283 mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos)); 284 r = ic.getWordRangeAtCursor(" ", 0); 285 suggestions = r.getSuggestionSpansAtWord(); 286 assertEquals(suggestions.length, 1); 287 MoreAsserts.assertEquals(suggestions[0].getSuggestions(), SUGGESTIONS1); 288 289 // Test a case with overlapping spans, 2nd extending past both ends of the word 290 text = new SpannableString("This is a string for test"); 291 text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS1, 0 /* flags */), 292 10 /* start */, 16 /* end */, 0 /* flags */); 293 text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS2, 0 /* flags */), 294 5 /* start */, 20 /* end */, 0 /* flags */); 295 mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos)); 296 r = ic.getWordRangeAtCursor(" ", 0); 297 suggestions = r.getSuggestionSpansAtWord(); 298 assertEquals(suggestions.length, 1); 299 MoreAsserts.assertEquals(suggestions[0].getSuggestions(), SUGGESTIONS1); 300 301 // Test a case with overlapping spans, none right on the word 302 text = new SpannableString("This is a string for test"); 303 text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS1, 0 /* flags */), 304 5 /* start */, 16 /* end */, 0 /* flags */); 305 text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS2, 0 /* flags */), 306 5 /* start */, 20 /* end */, 0 /* flags */); 307 mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos)); 308 r = ic.getWordRangeAtCursor(" ", 0); 309 suggestions = r.getSuggestionSpansAtWord(); 310 assertEquals(suggestions.length, 0); 311 } 312 } 313