Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 205a9939 authored by Roozbeh Pournader's avatar Roozbeh Pournader
Browse files

Internationalize InputFilter.AllCaps

The new code support non-BMP characters, as well as locale-specific
uppercasing and fine-grained span copying.

The modern capitalization code in AllCapsTransformationMethod is
moved to TextUtils now, so InputFilter.AllCaps can share it.

Fixes: 37222101
Test: New CTS and core tests are added.
Test: cts-tradefed run cts-dev --module CtsTextTestCases
Test: adb shell am instrument -w -e package android.text com.android.frameworks.coretests/android.support.test.runner.AndroidJUnitRunner

Change-Id: I021ff2a97a60396fb1b6e4940d91d3cd6ccb6196
parent eed27590
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -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);
  }
+1 −0
Original line number Diff line number Diff line
@@ -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);
  }
+1 −0
Original line number Diff line number Diff line
@@ -41319,6 +41319,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);
  }
+107 −21
Original line number Diff line number Diff line
@@ -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.
@@ -37,37 +41,119 @@ public interface InputFilter
     * 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 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();

                    if (source instanceof Spanned) {
                        SpannableString sp = new SpannableString(s);
                        TextUtils.copySpansFrom((Spanned) source,
                                                start, end, null, sp, 0);
                        return sp;
                    } else {
                        return s;
            final CharSequence wrapper = new CharSequenceWrapper(source, start, end);

            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
            }

            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;
            }
        }
    }

    /**
+71 −0
Original line number Diff line number Diff line
@@ -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,
Loading