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
17package android.text;
18
19import android.annotation.NonNull;
20
21import com.android.internal.util.Preconditions;
22
23import 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 */
29public 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     */
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
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         */
69        public AllCaps(@NonNull Locale locale) {
70            Preconditions.checkNotNull(locale);
71            mLocale = locale;
72        }
73
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
110            CharSequenceWrapper(CharSequence source, int start, int end) {
111                mSource = source;
112                mStart = start;
113                mEnd = end;
114                mLength = end - start;
115            }
116
117            public int length() {
118                return mLength;
119            }
120
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
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
135            public String toString() {
136                return mSource.subSequence(mStart, mEnd).toString();
137            }
138
139            public <T> T[] getSpans(int start, int end, Class<T> type) {
140                return ((Spanned) mSource).getSpans(mStart + start, mStart + end, type);
141            }
142
143            public int getSpanStart(Object tag) {
144                return ((Spanned) mSource).getSpanStart(tag) - mStart;
145            }
146
147            public int getSpanEnd(Object tag) {
148                return ((Spanned) mSource).getSpanEnd(tag) - mStart;
149            }
150
151            public int getSpanFlags(Object tag) {
152                return ((Spanned) mSource).getSpanFlags(tag);
153            }
154
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
169        public LengthFilter(int max) {
170            mMax = max;
171        }
172
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         */
195        public int getMax() {
196            return mMax;
197        }
198    }
199}
200