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