Merge "Internationalize InputFilter.AllCaps"

This commit is contained in:
Roozbeh Pournader
2017-06-09 22:23:42 +00:00
committed by Android (Google) Code Review
7 changed files with 274 additions and 85 deletions

View File

@@ -41102,6 +41102,7 @@ package android.text {
public static class InputFilter.AllCaps implements android.text.InputFilter {
ctor public InputFilter.AllCaps();
ctor public InputFilter.AllCaps(java.util.Locale);
method public java.lang.CharSequence filter(java.lang.CharSequence, int, int, android.text.Spanned, int, int);
}

View File

@@ -44647,6 +44647,7 @@ package android.text {
public static class InputFilter.AllCaps implements android.text.InputFilter {
ctor public InputFilter.AllCaps();
ctor public InputFilter.AllCaps(java.util.Locale);
method public java.lang.CharSequence filter(java.lang.CharSequence, int, int, android.text.Spanned, int, int);
}

View File

@@ -41329,6 +41329,7 @@ package android.text {
public static class InputFilter.AllCaps implements android.text.InputFilter {
ctor public InputFilter.AllCaps();
ctor public InputFilter.AllCaps(java.util.Locale);
method public java.lang.CharSequence filter(java.lang.CharSequence, int, int, android.text.Spanned, int, int);
}

View File

@@ -16,6 +16,10 @@
package android.text;
import android.annotation.Nullable;
import java.util.Locale;
/**
* InputFilters can be attached to {@link Editable}s to constrain the
* changes that can be made to them.
@@ -33,40 +37,122 @@ public interface InputFilter
* as this is what happens when you delete text. Also beware that
* you should not attempt to make any changes to <code>dest</code>
* from this method; you may only examine it for context.
*
*
* Note: If <var>source</var> is an instance of {@link Spanned} or
* {@link Spannable}, the span objects in the <var>source</var> should be
* copied into the filtered result (i.e. the non-null return value).
* {@link TextUtils#copySpansFrom} can be used for convenience.
* {@link Spannable}, the span objects in the <var>source</var> should be
* copied into the filtered result (i.e. the non-null return value).
* {@link TextUtils#copySpansFrom} can be used for convenience if the
* span boundary indices would be remaining identical relative to the source.
*/
public CharSequence filter(CharSequence source, int start, int end,
Spanned dest, int dstart, int dend);
/**
* This filter will capitalize all the lower case letters that are added
* through edits.
* This filter will capitalize all the lowercase and titlecase letters that are added
* through edits. (Note that if there are no lowercase or titlecase letters in the input, the
* text would not be transformed, even if the result of capitalization of the string is
* different from the string.)
*/
public static class AllCaps implements InputFilter {
private final Locale mLocale;
public AllCaps() {
mLocale = null;
}
/**
* Constructs a locale-specific AllCaps filter, to make sure capitalization rules of that
* locale are used for transforming the sequence.
*/
public AllCaps(@Nullable Locale locale) {
mLocale = locale;
}
public CharSequence filter(CharSequence source, int start, int end,
Spanned dest, int dstart, int dend) {
for (int i = start; i < end; i++) {
if (Character.isLowerCase(source.charAt(i))) {
char[] v = new char[end - start];
TextUtils.getChars(source, start, end, v, 0);
String s = new String(v).toUpperCase();
final CharSequence wrapper = new CharSequenceWrapper(source, start, end);
if (source instanceof Spanned) {
SpannableString sp = new SpannableString(s);
TextUtils.copySpansFrom((Spanned) source,
start, end, null, sp, 0);
return sp;
} else {
return s;
}
boolean lowerOrTitleFound = false;
final int length = end - start;
for (int i = 0, cp; i < length; i += Character.charCount(cp)) {
// We access 'wrapper' instead of 'source' to make sure no code unit beyond 'end' is
// ever accessed.
cp = Character.codePointAt(wrapper, i);
if (Character.isLowerCase(cp) || Character.isTitleCase(cp)) {
lowerOrTitleFound = true;
break;
}
}
if (!lowerOrTitleFound) {
return null; // keep original
}
return null; // keep original
final boolean copySpans = source instanceof Spanned;
final CharSequence upper = TextUtils.toUpperCase(mLocale, wrapper, copySpans);
if (upper == wrapper) {
// Nothing was changed in the uppercasing operation. This is weird, since
// we had found at least one lowercase or titlecase character. But we can't
// do anything better than keeping the original in this case.
return null; // keep original
}
// Return a SpannableString or String for backward compatibility.
return copySpans ? new SpannableString(upper) : upper.toString();
}
private static class CharSequenceWrapper implements CharSequence, Spanned {
private final CharSequence mSource;
private final int mStart, mEnd;
private final int mLength;
CharSequenceWrapper(CharSequence source, int start, int end) {
mSource = source;
mStart = start;
mEnd = end;
mLength = end - start;
}
public int length() {
return mLength;
}
public char charAt(int index) {
if (index < 0 || index >= mLength) {
throw new IndexOutOfBoundsException();
}
return mSource.charAt(mStart + index);
}
public CharSequence subSequence(int start, int end) {
if (start < 0 || end < 0 || end > mLength || start > end) {
throw new IndexOutOfBoundsException();
}
return new CharSequenceWrapper(mSource, mStart + start, mStart + end);
}
public String toString() {
return mSource.subSequence(mStart, mEnd).toString();
}
public <T> T[] getSpans(int start, int end, Class<T> type) {
return ((Spanned) mSource).getSpans(mStart + start, mStart + end, type);
}
public int getSpanStart(Object tag) {
return ((Spanned) mSource).getSpanStart(tag) - mStart;
}
public int getSpanEnd(Object tag) {
return ((Spanned) mSource).getSpanEnd(tag) - mStart;
}
public int getSpanFlags(Object tag) {
return ((Spanned) mSource).getSpanFlags(tag);
}
public int nextSpanTransition(int start, int limit, Class type) {
return ((Spanned) mSource).nextSpanTransition(mStart + start, mStart + limit, type)
- mStart;
}
}
}

View File

@@ -23,6 +23,8 @@ import android.annotation.PluralsRes;
import android.content.Context;
import android.content.res.Resources;
import android.icu.lang.UCharacter;
import android.icu.text.CaseMap;
import android.icu.text.Edits;
import android.icu.util.ULocale;
import android.os.Parcel;
import android.os.Parcelable;
@@ -1072,6 +1074,75 @@ public class TextUtils {
}
}
/**
* Transforms a CharSequences to uppercase, copying the sources spans and keeping them spans as
* much as possible close to their relative original places. In the case the the uppercase
* string is identical to the sources, the source itself is returned instead of being copied.
*
* If copySpans is set, source must be an instance of Spanned.
*
* {@hide}
*/
@NonNull
public static CharSequence toUpperCase(@Nullable Locale locale, @NonNull CharSequence source,
boolean copySpans) {
final Edits edits = new Edits();
if (!copySpans) { // No spans. Just uppercase the characters.
final StringBuilder result = CaseMap.toUpper().apply(
locale, source, new StringBuilder(), edits);
return edits.hasChanges() ? result : source;
}
final SpannableStringBuilder result = CaseMap.toUpper().apply(
locale, source, new SpannableStringBuilder(), edits);
if (!edits.hasChanges()) {
// No changes happened while capitalizing. We can return the source as it was.
return source;
}
final Edits.Iterator iterator = edits.getFineIterator();
final int sourceLength = source.length();
final Spanned spanned = (Spanned) source;
final Object[] spans = spanned.getSpans(0, sourceLength, Object.class);
for (Object span : spans) {
final int sourceStart = spanned.getSpanStart(span);
final int sourceEnd = spanned.getSpanEnd(span);
final int flags = spanned.getSpanFlags(span);
// Make sure the indices are not at the end of the string, since in that case
// iterator.findSourceIndex() would fail.
final int destStart = sourceStart == sourceLength ? result.length() :
toUpperMapToDest(iterator, sourceStart);
final int destEnd = sourceEnd == sourceLength ? result.length() :
toUpperMapToDest(iterator, sourceEnd);
result.setSpan(span, destStart, destEnd, flags);
}
return result;
}
// helper method for toUpperCase()
private static int toUpperMapToDest(Edits.Iterator iterator, int sourceIndex) {
// Guaranteed to succeed if sourceIndex < source.length().
iterator.findSourceIndex(sourceIndex);
if (sourceIndex == iterator.sourceIndex()) {
return iterator.destinationIndex();
}
// We handle the situation differently depending on if we are in the changed slice or an
// unchanged one: In an unchanged slice, we can find the exact location the span
// boundary was before and map there.
//
// But in a changed slice, we need to treat the whole destination slice as an atomic unit.
// We adjust the span boundary to the end of that slice to reduce of the chance of adjacent
// spans in the source overlapping in the result. (The choice for the end vs the beginning
// is somewhat arbitrary, but was taken because we except to see slightly more spans only
// affecting a base character compared to spans only affecting a combining character.)
if (iterator.hasChange()) {
return iterator.destinationIndex() + iterator.newLength();
} else {
// Move the index 1:1 along with this unchanged piece of text.
return iterator.destinationIndex() + (sourceIndex - iterator.sourceIndex());
}
}
public enum TruncateAt {
START,
MIDDLE,

View File

@@ -15,12 +15,12 @@
*/
package android.text.method;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.graphics.Rect;
import android.icu.text.CaseMap;
import android.icu.text.Edits;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
@@ -38,12 +38,12 @@ public class AllCapsTransformationMethod implements TransformationMethod2 {
private boolean mEnabled;
private Locale mLocale;
public AllCapsTransformationMethod(Context context) {
public AllCapsTransformationMethod(@NonNull Context context) {
mLocale = context.getResources().getConfiguration().getLocales().get(0);
}
@Override
public CharSequence getTransformation(CharSequence source, View view) {
public CharSequence getTransformation(@Nullable CharSequence source, View view) {
if (!mEnabled) {
Log.w(TAG, "Caller did not enable length changes; not transforming text");
return source;
@@ -60,61 +60,8 @@ public class AllCapsTransformationMethod implements TransformationMethod2 {
if (locale == null) {
locale = mLocale;
}
if (!(source instanceof Spanned)) { // No spans
return CaseMap.toUpper().apply(
locale, source, new StringBuilder(),
null /* we don't need the edits */);
}
final Edits edits = new Edits();
final SpannableStringBuilder result = CaseMap.toUpper().apply(
locale, source, new SpannableStringBuilder(), edits);
if (!edits.hasChanges()) {
// No changes happened while capitalizing. We can return the source as it was.
return source;
}
final Edits.Iterator iterator = edits.getFineIterator();
final Spanned spanned = (Spanned) source;
final int sourceLength = source.length();
final Object[] spans = spanned.getSpans(0, sourceLength, Object.class);
for (Object span : spans) {
final int sourceStart = spanned.getSpanStart(span);
final int sourceEnd = spanned.getSpanEnd(span);
final int flags = spanned.getSpanFlags(span);
// Make sure the indexes are not at the end of the string, since in that case
// iterator.findSourceIndex() would fail.
final int destStart = sourceStart == sourceLength ? result.length() :
mapToDest(iterator, sourceStart);
final int destEnd = sourceEnd == sourceLength ? result.length() :
mapToDest(iterator, sourceEnd);
result.setSpan(span, destStart, destEnd, flags);
}
return result;
}
private static int mapToDest(Edits.Iterator iterator, int sourceIndex) {
// Guaranteed to succeed if sourceIndex < source.length().
iterator.findSourceIndex(sourceIndex);
if (sourceIndex == iterator.sourceIndex()) {
return iterator.destinationIndex();
}
// We handle the situation differently depending on if we are in the changed slice or an
// unchanged one: In an unchanged slice, we can find the exact location the span
// boundary was before and map there.
//
// But in a changed slice, we need to treat the whole destination slice as an atomic unit.
// We adjust the span boundary to the end of that slice to reduce of the chance of adjacent
// spans in the source overlapping in the result. (The choice for the end vs the beginning
// is somewhat arbitrary, but was taken because we except to see slightly more spans only
// affecting a base character compared to spans only affecting a combining character.)
if (iterator.hasChange()) {
return iterator.destinationIndex() + iterator.newLength();
} else {
// Move the index 1:1 along with this unchanged piece of text.
return iterator.destinationIndex() + (sourceIndex - iterator.sourceIndex());
}
final boolean copySpans = source instanceof Spanned;
return TextUtils.toUpperCase(locale, source, copySpans);
}
@Override

View File

@@ -20,26 +20,28 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import android.support.test.runner.AndroidJUnit4;
import com.google.android.collect.Lists;
import android.os.Parcel;
import android.support.test.filters.LargeTest;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import android.test.MoreAsserts;
import android.text.style.StyleSpan;
import android.text.util.Rfc822Token;
import android.text.util.Rfc822Tokenizer;
import android.view.View;
import com.google.android.collect.Lists;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* TextUtilsTest tests {@link TextUtils}.
@@ -580,4 +582,84 @@ public class TextUtilsTest {
assertEquals(View.LAYOUT_DIRECTION_RTL,
TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("tr-Arab")));
}
@Test
public void testToUpperCase() {
{
final CharSequence result = TextUtils.toUpperCase(null, "abc", false);
assertEquals(StringBuilder.class, result.getClass());
assertEquals("ABC", result.toString());
}
{
final SpannableString str = new SpannableString("abc");
Object span = new Object();
str.setSpan(span, 1, 2, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
final CharSequence result = TextUtils.toUpperCase(null, str, true /* copySpans */);
assertEquals(SpannableStringBuilder.class, result.getClass());
assertEquals("ABC", result.toString());
final Spanned spanned = (Spanned) result;
final Object[] resultSpans = spanned.getSpans(0, result.length(), Object.class);
assertEquals(1, resultSpans.length);
assertSame(span, resultSpans[0]);
assertEquals(1, spanned.getSpanStart(span));
assertEquals(2, spanned.getSpanEnd(span));
assertEquals(Spanned.SPAN_INCLUSIVE_INCLUSIVE, spanned.getSpanFlags(span));
}
{
final Locale turkish = new Locale("tr", "TR");
final CharSequence result = TextUtils.toUpperCase(turkish, "i", false);
assertEquals(StringBuilder.class, result.getClass());
assertEquals("İ", result.toString());
}
{
final String str = "ABC";
assertSame(str, TextUtils.toUpperCase(null, str, false));
}
{
final SpannableString str = new SpannableString("ABC");
str.setSpan(new Object(), 1, 2, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
assertSame(str, TextUtils.toUpperCase(null, str, true /* copySpans */));
}
}
// Copied from cts/tests/tests/widget/src/android/widget/cts/TextViewTest.java and modified
// for the TextUtils.toUpperCase method.
@Test
public void testToUpperCase_SpansArePreserved() {
final Locale greek = new Locale("el", "GR");
final String lowerString = "ι\u0301ριδα"; // ίριδα with first letter decomposed
final String upperString = "ΙΡΙΔΑ"; // uppercased
// expected lowercase to uppercase index map
final int[] indexMap = {0, 1, 1, 2, 3, 4, 5};
final int flags = Spanned.SPAN_INCLUSIVE_INCLUSIVE;
final Spannable source = new SpannableString(lowerString);
source.setSpan(new Object(), 0, 1, flags);
source.setSpan(new Object(), 1, 2, flags);
source.setSpan(new Object(), 2, 3, flags);
source.setSpan(new Object(), 3, 4, flags);
source.setSpan(new Object(), 4, 5, flags);
source.setSpan(new Object(), 5, 6, flags);
source.setSpan(new Object(), 0, 2, flags);
source.setSpan(new Object(), 1, 3, flags);
source.setSpan(new Object(), 2, 4, flags);
source.setSpan(new Object(), 0, 6, flags);
final Object[] sourceSpans = source.getSpans(0, source.length(), Object.class);
final CharSequence uppercase = TextUtils.toUpperCase(greek, source, true /* copySpans */);
assertEquals(SpannableStringBuilder.class, uppercase.getClass());
final Spanned result = (Spanned) uppercase;
assertEquals(upperString, result.toString());
final Object[] resultSpans = result.getSpans(0, result.length(), Object.class);
assertEquals(sourceSpans.length, resultSpans.length);
for (int i = 0; i < sourceSpans.length; i++) {
assertSame(sourceSpans[i], resultSpans[i]);
final Object span = sourceSpans[i];
assertEquals(indexMap[source.getSpanStart(span)], result.getSpanStart(span));
assertEquals(indexMap[source.getSpanEnd(span)], result.getSpanEnd(span));
assertEquals(source.getSpanFlags(span), result.getSpanFlags(span));
}
}
}