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