1/* 2 * Copyright (C) 2010 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 android.widget; 18 19import android.test.AndroidTestCase; 20import android.test.suitebuilder.annotation.LargeTest; 21import android.text.InputType; 22import android.text.Selection; 23import android.text.Spannable; 24import android.text.SpannableString; 25 26import java.lang.reflect.Field; 27import java.lang.reflect.InvocationTargetException; 28import java.lang.reflect.Method; 29 30/** 31 * TextViewPatchTest tests {@link TextView}'s definition of word. Finds and 32 * verifies word limits to be in strings containing different kinds of 33 * characters. 34 */ 35public class TextViewWordLimitsTest extends AndroidTestCase { 36 37 TextView mTv = null; 38 Method mGetWordLimits, mSelectCurrentWord; 39 Field mContextMenuTriggeredByKey, mSelectionControllerEnabled; 40 41 42 /** 43 * Sets up common fields used in all test cases. 44 * @throws NoSuchFieldException 45 * @throws SecurityException 46 */ 47 @Override 48 protected void setUp() throws NoSuchMethodException, SecurityException, NoSuchFieldException { 49 mTv = new TextView(getContext()); 50 mTv.setInputType(InputType.TYPE_CLASS_TEXT); 51 52 mGetWordLimits = mTv.getClass().getDeclaredMethod("getWordLimitsAt", 53 new Class[] {int.class}); 54 mGetWordLimits.setAccessible(true); 55 56 mSelectCurrentWord = mTv.getClass().getDeclaredMethod("selectCurrentWord", new Class[] {}); 57 mSelectCurrentWord.setAccessible(true); 58 59 mContextMenuTriggeredByKey = mTv.getClass().getDeclaredField("mContextMenuTriggeredByKey"); 60 mContextMenuTriggeredByKey.setAccessible(true); 61 mSelectionControllerEnabled = mTv.getClass().getDeclaredField("mSelectionControllerEnabled"); 62 mSelectionControllerEnabled.setAccessible(true); 63 } 64 65 /** 66 * Calculate and verify word limits. Depends on the TextView implementation. 67 * Uses a private method and internal data representation. 68 * 69 * @param text Text to select a word from 70 * @param pos Position to expand word around 71 * @param correctStart Correct start position for the word 72 * @param correctEnd Correct end position for the word 73 * @throws InvocationTargetException 74 * @throws IllegalAccessException 75 * @throws IllegalArgumentException 76 * @throws InvocationTargetException 77 * @throws IllegalAccessException 78 */ 79 private void verifyWordLimits(String text, int pos, int correctStart, int correctEnd) 80 throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { 81 mTv.setText(text, TextView.BufferType.SPANNABLE); 82 83 long limits = (Long)mGetWordLimits.invoke(mTv, new Object[] {new Integer(pos)}); 84 int actualStart = (int)(limits >>> 32); 85 int actualEnd = (int)(limits & 0x00000000FFFFFFFFL); 86 assertEquals(correctStart, actualStart); 87 assertEquals(correctEnd, actualEnd); 88 } 89 90 91 private void verifySelectCurrentWord(Spannable text, int selectionStart, int selectionEnd, int correctStart, 92 int correctEnd) throws InvocationTargetException, IllegalAccessException { 93 mTv.setText(text, TextView.BufferType.SPANNABLE); 94 95 Selection.setSelection((Spannable)mTv.getText(), selectionStart, selectionEnd); 96 mContextMenuTriggeredByKey.setBoolean(mTv, true); 97 mSelectionControllerEnabled.setBoolean(mTv, true); 98 mSelectCurrentWord.invoke(mTv); 99 100 assertEquals(correctStart, mTv.getSelectionStart()); 101 assertEquals(correctEnd, mTv.getSelectionEnd()); 102 } 103 104 105 /** 106 * Corner cases for string length. 107 */ 108 @LargeTest 109 public void testLengths() throws Exception { 110 final String ONE_TWO = "one two"; 111 final String EMPTY = ""; 112 final String TOOLONG = "ThisWordIsTooLongToBeDefinedAsAWordInTheSenseUsedInTextView"; 113 114 // Select first word 115 verifyWordLimits(ONE_TWO, 0, 0, 3); 116 verifyWordLimits(ONE_TWO, 3, 0, 3); 117 118 // Select last word 119 verifyWordLimits(ONE_TWO, 4, 4, 7); 120 verifyWordLimits(ONE_TWO, 7, 4, 7); 121 122 // Empty string 123 verifyWordLimits(EMPTY, 0, -1, -1); 124 125 // Too long word 126 verifyWordLimits(TOOLONG, 0, -1, -1); 127 } 128 129 /** 130 * Unicode classes included. 131 */ 132 @LargeTest 133 public void testIncludedClasses() throws Exception { 134 final String LOWER = "njlj"; 135 final String UPPER = "NJLJ"; 136 final String TITLECASE = "\u01C8\u01CB\u01F2"; // Lj Nj Dz 137 final String OTHER = "\u3042\u3044\u3046"; // Hiragana AIU 138 final String MODIFIER = "\u02C6\u02CA\u02CB"; // Circumflex Acute Grave 139 140 // Each string contains a single valid word 141 verifyWordLimits(LOWER, 1, 0, 4); 142 verifyWordLimits(UPPER, 1, 0, 4); 143 verifyWordLimits(TITLECASE, 1, 0, 3); 144 verifyWordLimits(OTHER, 1, 0, 3); 145 verifyWordLimits(MODIFIER, 1, 0, 3); 146 } 147 148 /** 149 * Unicode classes included if combined with a letter. 150 */ 151 @LargeTest 152 public void testPartlyIncluded() throws Exception { 153 final String NUMBER = "123"; 154 final String NUMBER_LOWER = "1st"; 155 final String APOSTROPHE = "''"; 156 final String APOSTROPHE_LOWER = "'Android's'"; 157 158 // Pure decimal number is ignored 159 verifyWordLimits(NUMBER, 1, -1, -1); 160 161 // Number with letter is valid 162 verifyWordLimits(NUMBER_LOWER, 1, 0, 3); 163 164 // Stand apostrophes are ignore 165 verifyWordLimits(APOSTROPHE, 1, -1, -1); 166 167 // Apostrophes are accepted if they are a part of a word 168 verifyWordLimits(APOSTROPHE_LOWER, 1, 0, 11); 169 } 170 171 /** 172 * Unicode classes included if combined with a letter. 173 */ 174 @LargeTest 175 public void testFinalSeparator() throws Exception { 176 final String PUNCTUATION = "abc, def."; 177 178 // Starting from the comma 179 verifyWordLimits(PUNCTUATION, 3, 0, 3); 180 verifyWordLimits(PUNCTUATION, 4, 0, 4); 181 182 // Starting from the final period 183 verifyWordLimits(PUNCTUATION, 8, 5, 8); 184 verifyWordLimits(PUNCTUATION, 9, 5, 9); 185 } 186 187 /** 188 * Unicode classes other than listed in testIncludedClasses and 189 * testPartlyIncluded act as word separators. 190 */ 191 @LargeTest 192 public void testNotIncluded() throws Exception { 193 // Selection of character classes excluded 194 final String MARK_NONSPACING = "a\u030A"; // a Combining ring above 195 final String PUNCTUATION_OPEN_CLOSE = "(c)"; // Parenthesis 196 final String PUNCTUATION_DASH = "non-fiction"; // Hyphen 197 final String PUNCTUATION_OTHER = "b&b"; // Ampersand 198 final String SYMBOL_OTHER = "Android\u00AE"; // Registered 199 final String SEPARATOR_SPACE = "one two"; // Space 200 201 // "a" 202 verifyWordLimits(MARK_NONSPACING, 1, 0, 1); 203 204 // "c" 205 verifyWordLimits(PUNCTUATION_OPEN_CLOSE, 1, 1, 2); 206 207 // "non-" 208 verifyWordLimits(PUNCTUATION_DASH, 3, 0, 3); 209 verifyWordLimits(PUNCTUATION_DASH, 4, 4, 11); 210 211 // "b" 212 verifyWordLimits(PUNCTUATION_OTHER, 0, 0, 1); 213 verifyWordLimits(PUNCTUATION_OTHER, 1, 0, 1); 214 verifyWordLimits(PUNCTUATION_OTHER, 2, 0, 3); // & is considered a punctuation sign. 215 verifyWordLimits(PUNCTUATION_OTHER, 3, 2, 3); 216 217 // "Android" 218 verifyWordLimits(SYMBOL_OTHER, 7, 0, 7); 219 verifyWordLimits(SYMBOL_OTHER, 8, -1, -1); 220 221 // "one" 222 verifyWordLimits(SEPARATOR_SPACE, 1, 0, 3); 223 } 224 225 /** 226 * Surrogate characters are treated as their code points. 227 */ 228 @LargeTest 229 public void testSurrogate() throws Exception { 230 final String SURROGATE_LETTER = "\uD800\uDC00\uD800\uDC01\uD800\uDC02"; // Linear B AEI 231 final String SURROGATE_SYMBOL = "\uD83D\uDE01\uD83D\uDE02\uD83D\uDE03"; // Three smileys 232 233 // Letter Other is included even when coded as surrogate pairs 234 verifyWordLimits(SURROGATE_LETTER, 0, 0, 6); 235 verifyWordLimits(SURROGATE_LETTER, 1, 0, 6); 236 verifyWordLimits(SURROGATE_LETTER, 2, 0, 6); 237 verifyWordLimits(SURROGATE_LETTER, 3, 0, 6); 238 verifyWordLimits(SURROGATE_LETTER, 4, 0, 6); 239 verifyWordLimits(SURROGATE_LETTER, 5, 0, 6); 240 verifyWordLimits(SURROGATE_LETTER, 6, 0, 6); 241 242 // Not included classes are ignored even when coded as surrogate pairs 243 verifyWordLimits(SURROGATE_SYMBOL, 0, -1, -1); 244 verifyWordLimits(SURROGATE_SYMBOL, 1, -1, -1); 245 verifyWordLimits(SURROGATE_SYMBOL, 2, -1, -1); 246 verifyWordLimits(SURROGATE_SYMBOL, 3, -1, -1); 247 verifyWordLimits(SURROGATE_SYMBOL, 4, -1, -1); 248 verifyWordLimits(SURROGATE_SYMBOL, 5, -1, -1); 249 verifyWordLimits(SURROGATE_SYMBOL, 6, -1, -1); 250 } 251 252 /** 253 * Selection is used if present and valid word. 254 */ 255 @LargeTest 256 public void testSelectCurrentWord() throws Exception { 257 SpannableString textLower = new SpannableString("first second"); 258 SpannableString textOther = new SpannableString("\u3042\3044\3046"); // Hiragana AIU 259 SpannableString textDash = new SpannableString("non-fiction"); // Hyphen 260 SpannableString textPunctOther = new SpannableString("b&b"); // Ampersand 261 SpannableString textSymbolOther = new SpannableString("Android\u00AE"); // Registered 262 263 // Valid selection - Letter, Lower 264 verifySelectCurrentWord(textLower, 2, 5, 0, 5); 265 266 // Adding the space spreads to the second word 267 verifySelectCurrentWord(textLower, 2, 6, 0, 12); 268 269 // Valid selection -- Letter, Other 270 verifySelectCurrentWord(textOther, 1, 2, 0, 5); 271 272 // Zero-width selection is interpreted as a cursor and the selection is ignored 273 verifySelectCurrentWord(textLower, 2, 2, 0, 5); 274 275 // Hyphen is part of selection 276 verifySelectCurrentWord(textDash, 2, 5, 0, 11); 277 278 // Ampersand part of selection or not 279 verifySelectCurrentWord(textPunctOther, 1, 2, 0, 3); 280 verifySelectCurrentWord(textPunctOther, 1, 3, 0, 3); 281 282 // (R) part of the selection 283 verifySelectCurrentWord(textSymbolOther, 2, 7, 0, 7); 284 verifySelectCurrentWord(textSymbolOther, 2, 8, 0, 8); 285 } 286} 287