/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.widget;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.LargeTest;
import android.text.InputType;
import android.text.Selection;
import android.text.Spannable;
import android.text.SpannableString;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* TextViewPatchTest tests {@link TextView}'s definition of word. Finds and
* verifies word limits to be in strings containing different kinds of
* characters.
*/
public class TextViewWordLimitsTest extends AndroidTestCase {
TextView mTv = null;
Method mGetWordLimits, mSelectCurrentWord;
Field mContextMenuTriggeredByKey, mSelectionControllerEnabled;
/**
* Sets up common fields used in all test cases.
* @throws NoSuchFieldException
* @throws SecurityException
*/
@Override
protected void setUp() throws NoSuchMethodException, SecurityException, NoSuchFieldException {
mTv = new TextView(getContext());
mTv.setInputType(InputType.TYPE_CLASS_TEXT);
mGetWordLimits = mTv.getClass().getDeclaredMethod("getWordLimitsAt",
new Class[] {int.class});
mGetWordLimits.setAccessible(true);
mSelectCurrentWord = mTv.getClass().getDeclaredMethod("selectCurrentWord", new Class[] {});
mSelectCurrentWord.setAccessible(true);
mContextMenuTriggeredByKey = mTv.getClass().getDeclaredField("mContextMenuTriggeredByKey");
mContextMenuTriggeredByKey.setAccessible(true);
mSelectionControllerEnabled = mTv.getClass().getDeclaredField("mSelectionControllerEnabled");
mSelectionControllerEnabled.setAccessible(true);
}
/**
* Calculate and verify word limits. Depends on the TextView implementation.
* Uses a private method and internal data representation.
*
* @param text Text to select a word from
* @param pos Position to expand word around
* @param correctStart Correct start position for the word
* @param correctEnd Correct end position for the word
* @throws InvocationTargetException
* @throws IllegalAccessException
* @throws IllegalArgumentException
* @throws InvocationTargetException
* @throws IllegalAccessException
*/
private void verifyWordLimits(String text, int pos, int correctStart, int correctEnd)
throws IllegalArgumentException, IllegalAccessException, InvocationTargetException {
mTv.setText(text, TextView.BufferType.SPANNABLE);
long limits = (Long)mGetWordLimits.invoke(mTv, new Object[] {new Integer(pos)});
int actualStart = (int)(limits >>> 32);
int actualEnd = (int)(limits & 0x00000000FFFFFFFFL);
assertEquals(correctStart, actualStart);
assertEquals(correctEnd, actualEnd);
}
private void verifySelectCurrentWord(Spannable text, int selectionStart, int selectionEnd, int correctStart,
int correctEnd) throws InvocationTargetException, IllegalAccessException {
mTv.setText(text, TextView.BufferType.SPANNABLE);
Selection.setSelection((Spannable)mTv.getText(), selectionStart, selectionEnd);
mContextMenuTriggeredByKey.setBoolean(mTv, true);
mSelectionControllerEnabled.setBoolean(mTv, true);
mSelectCurrentWord.invoke(mTv);
assertEquals(correctStart, mTv.getSelectionStart());
assertEquals(correctEnd, mTv.getSelectionEnd());
}
/**
* Corner cases for string length.
*/
@LargeTest
public void testLengths() throws Exception {
final String ONE_TWO = "one two";
final String EMPTY = "";
final String TOOLONG = "ThisWordIsTooLongToBeDefinedAsAWordInTheSenseUsedInTextView";
// Select first word
verifyWordLimits(ONE_TWO, 0, 0, 3);
verifyWordLimits(ONE_TWO, 3, 0, 3);
// Select last word
verifyWordLimits(ONE_TWO, 4, 4, 7);
verifyWordLimits(ONE_TWO, 7, 4, 7);
// Empty string
verifyWordLimits(EMPTY, 0, -1, -1);
// Too long word
verifyWordLimits(TOOLONG, 0, -1, -1);
}
/**
* Unicode classes included.
*/
@LargeTest
public void testIncludedClasses() throws Exception {
final String LOWER = "njlj";
final String UPPER = "NJLJ";
final String TITLECASE = "\u01C8\u01CB\u01F2"; // Lj Nj Dz
final String OTHER = "\u3042\u3044\u3046"; // Hiragana AIU
final String MODIFIER = "\u02C6\u02CA\u02CB"; // Circumflex Acute Grave
// Each string contains a single valid word
verifyWordLimits(LOWER, 1, 0, 4);
verifyWordLimits(UPPER, 1, 0, 4);
verifyWordLimits(TITLECASE, 1, 0, 3);
verifyWordLimits(OTHER, 1, 0, 3);
verifyWordLimits(MODIFIER, 1, 0, 3);
}
/**
* Unicode classes included if combined with a letter.
*/
@LargeTest
public void testPartlyIncluded() throws Exception {
final String NUMBER = "123";
final String NUMBER_LOWER = "1st";
final String APOSTROPHE = "''";
final String APOSTROPHE_LOWER = "'Android's'";
// Pure decimal number is ignored
verifyWordLimits(NUMBER, 1, -1, -1);
// Number with letter is valid
verifyWordLimits(NUMBER_LOWER, 1, 0, 3);
// Stand apostrophes are ignore
verifyWordLimits(APOSTROPHE, 1, -1, -1);
// Apostrophes are accepted if they are a part of a word
verifyWordLimits(APOSTROPHE_LOWER, 1, 0, 11);
}
/**
* Unicode classes included if combined with a letter.
*/
@LargeTest
public void testFinalSeparator() throws Exception {
final String PUNCTUATION = "abc, def.";
// Starting from the comma
verifyWordLimits(PUNCTUATION, 3, 0, 3);
verifyWordLimits(PUNCTUATION, 4, 0, 4);
// Starting from the final period
verifyWordLimits(PUNCTUATION, 8, 5, 8);
verifyWordLimits(PUNCTUATION, 9, 5, 9);
}
/**
* Unicode classes other than listed in testIncludedClasses and
* testPartlyIncluded act as word separators.
*/
@LargeTest
public void testNotIncluded() throws Exception {
// Selection of character classes excluded
final String MARK_NONSPACING = "a\u030A"; // a Combining ring above
final String PUNCTUATION_OPEN_CLOSE = "(c)"; // Parenthesis
final String PUNCTUATION_DASH = "non-fiction"; // Hyphen
final String PUNCTUATION_OTHER = "b&b"; // Ampersand
final String SYMBOL_OTHER = "Android\u00AE"; // Registered
final String SEPARATOR_SPACE = "one two"; // Space
// "a"
verifyWordLimits(MARK_NONSPACING, 1, 0, 1);
// "c"
verifyWordLimits(PUNCTUATION_OPEN_CLOSE, 1, 1, 2);
// "non-"
verifyWordLimits(PUNCTUATION_DASH, 3, 0, 3);
verifyWordLimits(PUNCTUATION_DASH, 4, 4, 11);
// "b"
verifyWordLimits(PUNCTUATION_OTHER, 0, 0, 1);
verifyWordLimits(PUNCTUATION_OTHER, 1, 0, 1);
verifyWordLimits(PUNCTUATION_OTHER, 2, 0, 3); // & is considered a punctuation sign.
verifyWordLimits(PUNCTUATION_OTHER, 3, 2, 3);
// "Android"
verifyWordLimits(SYMBOL_OTHER, 7, 0, 7);
verifyWordLimits(SYMBOL_OTHER, 8, -1, -1);
// "one"
verifyWordLimits(SEPARATOR_SPACE, 1, 0, 3);
}
/**
* Surrogate characters are treated as their code points.
*/
@LargeTest
public void testSurrogate() throws Exception {
final String SURROGATE_LETTER = "\uD800\uDC00\uD800\uDC01\uD800\uDC02"; // Linear B AEI
final String SURROGATE_SYMBOL = "\uD83D\uDE01\uD83D\uDE02\uD83D\uDE03"; // Three smileys
// Letter Other is included even when coded as surrogate pairs
verifyWordLimits(SURROGATE_LETTER, 0, 0, 6);
verifyWordLimits(SURROGATE_LETTER, 1, 0, 6);
verifyWordLimits(SURROGATE_LETTER, 2, 0, 6);
verifyWordLimits(SURROGATE_LETTER, 3, 0, 6);
verifyWordLimits(SURROGATE_LETTER, 4, 0, 6);
verifyWordLimits(SURROGATE_LETTER, 5, 0, 6);
verifyWordLimits(SURROGATE_LETTER, 6, 0, 6);
// Not included classes are ignored even when coded as surrogate pairs
verifyWordLimits(SURROGATE_SYMBOL, 0, -1, -1);
verifyWordLimits(SURROGATE_SYMBOL, 1, -1, -1);
verifyWordLimits(SURROGATE_SYMBOL, 2, -1, -1);
verifyWordLimits(SURROGATE_SYMBOL, 3, -1, -1);
verifyWordLimits(SURROGATE_SYMBOL, 4, -1, -1);
verifyWordLimits(SURROGATE_SYMBOL, 5, -1, -1);
verifyWordLimits(SURROGATE_SYMBOL, 6, -1, -1);
}
/**
* Selection is used if present and valid word.
*/
@LargeTest
public void testSelectCurrentWord() throws Exception {
SpannableString textLower = new SpannableString("first second");
SpannableString textOther = new SpannableString("\u3042\3044\3046"); // Hiragana AIU
SpannableString textDash = new SpannableString("non-fiction"); // Hyphen
SpannableString textPunctOther = new SpannableString("b&b"); // Ampersand
SpannableString textSymbolOther = new SpannableString("Android\u00AE"); // Registered
// Valid selection - Letter, Lower
verifySelectCurrentWord(textLower, 2, 5, 0, 5);
// Adding the space spreads to the second word
verifySelectCurrentWord(textLower, 2, 6, 0, 12);
// Valid selection -- Letter, Other
verifySelectCurrentWord(textOther, 1, 2, 0, 5);
// Zero-width selection is interpreted as a cursor and the selection is ignored
verifySelectCurrentWord(textLower, 2, 2, 0, 5);
// Hyphen is part of selection
verifySelectCurrentWord(textDash, 2, 5, 0, 11);
// Ampersand part of selection or not
verifySelectCurrentWord(textPunctOther, 1, 2, 0, 3);
verifySelectCurrentWord(textPunctOther, 1, 3, 0, 3);
// (R) part of the selection
verifySelectCurrentWord(textSymbolOther, 2, 7, 0, 7);
verifySelectCurrentWord(textSymbolOther, 2, 8, 0, 8);
}
}