blob: 96e7bd0fef4cbf64cba0c2ace578ac9f894e3933 [file] [log] [blame]
/*
* 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 android.compat.annotation.UnsupportedAppUsage;
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 <code>dstart &hellip; dend</code> of <code>dest</code>
* with the new text from the range <code>start &hellip; end</code>
* of <code>source</code>. Return the CharSequence that you would
* like to have placed there instead, including an empty string
* if appropriate, or <code>null</code> 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 <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 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> 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;
}
}
}
/**
* This filter will constrain edits not to make the length of the text
* greater than the specified length.
*/
public static class LengthFilter implements InputFilter {
@UnsupportedAppUsage
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;
}
}
}