/* * Copyright (C) 2006 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.text; import android.annotation.NonNull; import com.android.internal.util.Preconditions; import java.util.Locale; /** * InputFilters can be attached to {@link Editable}s to constrain the * changes that can be made to them. */ public interface InputFilter { /** * This method is called when the buffer is going to replace the * range dstart … dend of dest * with the new text from the range start … end * of source. Return the CharSequence that you would * like to have placed there instead, including an empty string * if appropriate, or null to accept the original * replacement. Be careful to not to reject 0-length replacements, * as this is what happens when you delete text. Also beware that * you should not attempt to make any changes to dest * from this method; you may only examine it for context. * * Note: If source is an instance of {@link Spanned} or * {@link Spannable}, the span objects in the source 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 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(@NonNull Locale locale) { Preconditions.checkNotNull(locale); mLocale = locale; } public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) { 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[] getSpans(int start, int end, Class 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; } } } /** * This filter will constrain edits not to make the length of the text * greater than the specified length. */ public static class LengthFilter implements InputFilter { private final int mMax; public LengthFilter(int max) { mMax = max; } public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) { int keep = mMax - (dest.length() - (dend - dstart)); if (keep <= 0) { return ""; } else if (keep >= end - start) { return null; // keep original } else { keep += start; if (Character.isHighSurrogate(source.charAt(keep - 1))) { --keep; if (keep == start) { return ""; } } return source.subSequence(start, keep); } } /** * @return the maximum length enforced by this input filter */ public int getMax() { return mMax; } } }