1 /*
2  * Copyright (C) 2006 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.text;
18 
19 import android.annotation.NonNull;
20 import android.compat.annotation.UnsupportedAppUsage;
21 
22 import com.android.internal.util.Preconditions;
23 
24 import java.util.Locale;
25 
26 /**
27  * InputFilters can be attached to {@link Editable}s to constrain the
28  * changes that can be made to them.
29  */
30 public interface InputFilter
31 {
32     /**
33      * This method is called when the buffer is going to replace the
34      * range <code>dstart &hellip; dend</code> of <code>dest</code>
35      * with the new text from the range <code>start &hellip; end</code>
36      * of <code>source</code>.  Return the CharSequence that you would
37      * like to have placed there instead, including an empty string
38      * if appropriate, or <code>null</code> to accept the original
39      * replacement.  Be careful to not to reject 0-length replacements,
40      * as this is what happens when you delete text.  Also beware that
41      * you should not attempt to make any changes to <code>dest</code>
42      * from this method; you may only examine it for context.
43      *
44      * Note: If <var>source</var> is an instance of {@link Spanned} or
45      * {@link Spannable}, the span objects in the <var>source</var> should be
46      * copied into the filtered result (i.e. the non-null return value).
47      * {@link TextUtils#copySpansFrom} can be used for convenience if the
48      * span boundary indices would be remaining identical relative to the source.
49      */
filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend)50     public CharSequence filter(CharSequence source, int start, int end,
51                                Spanned dest, int dstart, int dend);
52 
53     /**
54      * This filter will capitalize all the lowercase and titlecase letters that are added
55      * through edits. (Note that if there are no lowercase or titlecase letters in the input, the
56      * text would not be transformed, even if the result of capitalization of the string is
57      * different from the string.)
58      */
59     public static class AllCaps implements InputFilter {
60         private final Locale mLocale;
61 
AllCaps()62         public AllCaps() {
63             mLocale = null;
64         }
65 
66         /**
67          * Constructs a locale-specific AllCaps filter, to make sure capitalization rules of that
68          * locale are used for transforming the sequence.
69          */
AllCaps(@onNull Locale locale)70         public AllCaps(@NonNull Locale locale) {
71             Preconditions.checkNotNull(locale);
72             mLocale = locale;
73         }
74 
filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend)75         public CharSequence filter(CharSequence source, int start, int end,
76                                    Spanned dest, int dstart, int dend) {
77             final CharSequence wrapper = new CharSequenceWrapper(source, start, end);
78 
79             boolean lowerOrTitleFound = false;
80             final int length = end - start;
81             for (int i = 0, cp; i < length; i += Character.charCount(cp)) {
82                 // We access 'wrapper' instead of 'source' to make sure no code unit beyond 'end' is
83                 // ever accessed.
84                 cp = Character.codePointAt(wrapper, i);
85                 if (Character.isLowerCase(cp) || Character.isTitleCase(cp)) {
86                     lowerOrTitleFound = true;
87                     break;
88                 }
89             }
90             if (!lowerOrTitleFound) {
91                 return null; // keep original
92             }
93 
94             final boolean copySpans = source instanceof Spanned;
95             final CharSequence upper = TextUtils.toUpperCase(mLocale, wrapper, copySpans);
96             if (upper == wrapper) {
97                 // Nothing was changed in the uppercasing operation. This is weird, since
98                 // we had found at least one lowercase or titlecase character. But we can't
99                 // do anything better than keeping the original in this case.
100                 return null; // keep original
101             }
102             // Return a SpannableString or String for backward compatibility.
103             return copySpans ? new SpannableString(upper) : upper.toString();
104         }
105 
106         private static class CharSequenceWrapper implements CharSequence, Spanned {
107             private final CharSequence mSource;
108             private final int mStart, mEnd;
109             private final int mLength;
110 
CharSequenceWrapper(CharSequence source, int start, int end)111             CharSequenceWrapper(CharSequence source, int start, int end) {
112                 mSource = source;
113                 mStart = start;
114                 mEnd = end;
115                 mLength = end - start;
116             }
117 
length()118             public int length() {
119                 return mLength;
120             }
121 
charAt(int index)122             public char charAt(int index) {
123                 if (index < 0 || index >= mLength) {
124                     throw new IndexOutOfBoundsException();
125                 }
126                 return mSource.charAt(mStart + index);
127             }
128 
subSequence(int start, int end)129             public CharSequence subSequence(int start, int end) {
130                 if (start < 0 || end < 0 || end > mLength || start > end) {
131                     throw new IndexOutOfBoundsException();
132                 }
133                 return new CharSequenceWrapper(mSource, mStart + start, mStart + end);
134             }
135 
toString()136             public String toString() {
137                 return mSource.subSequence(mStart, mEnd).toString();
138             }
139 
getSpans(int start, int end, Class<T> type)140             public <T> T[] getSpans(int start, int end, Class<T> type) {
141                 return ((Spanned) mSource).getSpans(mStart + start, mStart + end, type);
142             }
143 
getSpanStart(Object tag)144             public int getSpanStart(Object tag) {
145                 return ((Spanned) mSource).getSpanStart(tag) - mStart;
146             }
147 
getSpanEnd(Object tag)148             public int getSpanEnd(Object tag) {
149                 return ((Spanned) mSource).getSpanEnd(tag) - mStart;
150             }
151 
getSpanFlags(Object tag)152             public int getSpanFlags(Object tag) {
153                 return ((Spanned) mSource).getSpanFlags(tag);
154             }
155 
nextSpanTransition(int start, int limit, Class type)156             public int nextSpanTransition(int start, int limit, Class type) {
157                 return ((Spanned) mSource).nextSpanTransition(mStart + start, mStart + limit, type)
158                         - mStart;
159             }
160         }
161     }
162 
163     /**
164      * This filter will constrain edits not to make the length of the text
165      * greater than the specified length.
166      */
167     public static class LengthFilter implements InputFilter {
168         @UnsupportedAppUsage
169         private final int mMax;
170 
LengthFilter(int max)171         public LengthFilter(int max) {
172             mMax = max;
173         }
174 
filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend)175         public CharSequence filter(CharSequence source, int start, int end, Spanned dest,
176                 int dstart, int dend) {
177             int keep = mMax - (dest.length() - (dend - dstart));
178             if (keep <= 0) {
179                 return "";
180             } else if (keep >= end - start) {
181                 return null; // keep original
182             } else {
183                 keep += start;
184                 if (Character.isHighSurrogate(source.charAt(keep - 1))) {
185                     --keep;
186                     if (keep == start) {
187                         return "";
188                     }
189                 }
190                 return source.subSequence(start, keep);
191             }
192         }
193 
194         /**
195          * @return the maximum length enforced by this input filter
196          */
getMax()197         public int getMax() {
198             return mMax;
199         }
200     }
201 }
202