summaryrefslogtreecommitdiffstats
path: root/core
diff options
context:
space:
mode:
authorGilles Debunne <debunne@google.com>2011-01-04 13:24:54 -0800
committerGilles Debunne <debunne@google.com>2011-01-04 14:18:13 -0800
commit87380bcaebe63bdcd44828f137b2b2b0ba952f0a (patch)
tree52e576cb0b85a3697fd648d03e95a9fd97518b09 /core
parent1451862b0af3a2d7df5f0eb2878863a583922177 (diff)
downloadframeworks_base-87380bcaebe63bdcd44828f137b2b2b0ba952f0a.zip
frameworks_base-87380bcaebe63bdcd44828f137b2b2b0ba952f0a.tar.gz
frameworks_base-87380bcaebe63bdcd44828f137b2b2b0ba952f0a.tar.bz2
Added support for asian characters in text selection.
Inspired by https://review.source.android.com/#change,16606 Test class has been revamped to mimic new behavior: selectCurrentWord is no longer used to add words to the dictionary. We rely on the suggestion bar in the IME for that. Change-Id: I1cb88df54dffb166c75f75fefb743ff55a33519b
Diffstat (limited to 'core')
-rw-r--r--core/java/android/widget/TextView.java16
-rw-r--r--core/tests/coretests/src/android/widget/TextViewWordLimitsTest.java276
2 files changed, 284 insertions, 8 deletions
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 320aef0..76d4835 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -7639,6 +7639,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
type == Character.LOWERCASE_LETTER ||
type == Character.TITLECASE_LETTER ||
type == Character.MODIFIER_LETTER ||
+ type == Character.OTHER_LETTER || // Should handle asian characters
type == Character.DECIMAL_DIGIT_NUMBER);
}
@@ -7647,15 +7648,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
*
* @param offset An offset in the text.
* @return The offsets for the start and end of the word located at <code>offset</code>.
- * The two ints offsets are packed in a long, with the starting offset shifted by 32 bits.
- * Returns a negative value if no valid word was found.
+ * The two ints offsets are packed in a long using {@link #packRangeInLong(int, int)}.
+ * Returns -1 if no valid word was found.
*/
private long getWordLimitsAt(int offset) {
int klass = mInputType & InputType.TYPE_MASK_CLASS;
int variation = mInputType & InputType.TYPE_MASK_VARIATION;
// Text selection is not permitted in password fields
- if (isPasswordInputType(mInputType) || isVisiblePasswordInputType(mInputType)) {
+ if (hasPasswordTransformationMethod()) {
return -1;
}
@@ -7739,17 +7740,16 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
if (hasPasswordTransformationMethod()) {
- // selectCurrentWord is not available on a password field and would return an
- // arbitrary 10-charater selection around pressed position. Select all instead.
+ // Always select all on a password field.
// Cut/copy menu entries are not available for passwords, but being able to select all
// is however useful to delete or paste to replace the entire content.
selectAll();
return;
}
- long lastTouchOffset = getLastTouchOffsets();
- final int minOffset = extractRangeStartFromLong(lastTouchOffset);
- final int maxOffset = extractRangeEndFromLong(lastTouchOffset);
+ long lastTouchOffsets = getLastTouchOffsets();
+ final int minOffset = extractRangeStartFromLong(lastTouchOffsets);
+ final int maxOffset = extractRangeEndFromLong(lastTouchOffsets);
int selectionStart, selectionEnd;
diff --git a/core/tests/coretests/src/android/widget/TextViewWordLimitsTest.java b/core/tests/coretests/src/android/widget/TextViewWordLimitsTest.java
new file mode 100644
index 0000000..fbc9e1f
--- /dev/null
+++ b/core/tests/coretests/src/android/widget/TextViewWordLimitsTest.java
@@ -0,0 +1,276 @@
+/*
+ * 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, 1, -1, -1);
+ verifyWordLimits(SURROGATE_LETTER, 2, -1, -1);
+
+ // Not included classes are ignored even when coded as surrogate pairs
+ verifyWordLimits(SURROGATE_SYMBOL, 1, -1, -1);
+ verifyWordLimits(SURROGATE_SYMBOL, 2, -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);
+ }
+}