• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.method;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.icu.text.DecimalFormatSymbols;
22 import android.text.Editable;
23 import android.text.InputFilter;
24 import android.text.Selection;
25 import android.text.Spannable;
26 import android.text.SpannableStringBuilder;
27 import android.text.Spanned;
28 import android.text.format.DateFormat;
29 import android.view.KeyEvent;
30 import android.view.View;
31 
32 import java.util.Collection;
33 import java.util.Locale;
34 
35 /**
36  * For numeric text entry
37  * <p></p>
38  * As for all implementations of {@link KeyListener}, this class is only concerned
39  * with hardware keyboards.  Software input methods have no obligation to trigger
40  * the methods in this class.
41  */
42 @android.ravenwood.annotation.RavenwoodKeepWholeClass
43 public abstract class NumberKeyListener extends BaseKeyListener
44     implements InputFilter
45 {
46     /**
47      * You can say which characters you can accept.
48      */
49     @NonNull
getAcceptedChars()50     protected abstract char[] getAcceptedChars();
51 
lookup(KeyEvent event, Spannable content)52     protected int lookup(KeyEvent event, Spannable content) {
53         return event.getMatch(getAcceptedChars(), getMetaState(content, event));
54     }
55 
filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend)56     public CharSequence filter(CharSequence source, int start, int end,
57                                Spanned dest, int dstart, int dend) {
58         char[] accept = getAcceptedChars();
59         boolean filter = false;
60 
61         int i;
62         for (i = start; i < end; i++) {
63             if (!ok(accept, source.charAt(i))) {
64                 break;
65             }
66         }
67 
68         if (i == end) {
69             // It was all OK.
70             return null;
71         }
72 
73         if (end - start == 1) {
74             // It was not OK, and there is only one char, so nothing remains.
75             return "";
76         }
77 
78         SpannableStringBuilder filtered =
79             new SpannableStringBuilder(source, start, end);
80         i -= start;
81         end -= start;
82 
83         int len = end - start;
84         // Only count down to i because the chars before that were all OK.
85         for (int j = end - 1; j >= i; j--) {
86             if (!ok(accept, source.charAt(j))) {
87                 filtered.delete(j, j + 1);
88             }
89         }
90 
91         return filtered;
92     }
93 
ok(char[] accept, char c)94     protected static boolean ok(char[] accept, char c) {
95         for (int i = accept.length - 1; i >= 0; i--) {
96             if (accept[i] == c) {
97                 return true;
98             }
99         }
100 
101         return false;
102     }
103 
104     @Override
onKeyDown(View view, Editable content, int keyCode, KeyEvent event)105     public boolean onKeyDown(View view, Editable content,
106                              int keyCode, KeyEvent event) {
107         int selStart, selEnd;
108 
109         {
110             int a = Selection.getSelectionStart(content);
111             int b = Selection.getSelectionEnd(content);
112 
113             selStart = Math.min(a, b);
114             selEnd = Math.max(a, b);
115         }
116 
117         if (selStart < 0 || selEnd < 0) {
118             selStart = selEnd = 0;
119             Selection.setSelection(content, 0);
120         }
121 
122         int i = event != null ? lookup(event, content) : 0;
123         int repeatCount = event != null ? event.getRepeatCount() : 0;
124         if (repeatCount == 0) {
125             if (i != 0) {
126                 if (selStart != selEnd) {
127                     Selection.setSelection(content, selEnd);
128                 }
129 
130                 content.replace(selStart, selEnd, String.valueOf((char) i));
131 
132                 adjustMetaAfterKeypress(content);
133                 return true;
134             }
135         } else if (i == '0' && repeatCount == 1) {
136             // Pretty hackish, it replaces the 0 with the +
137 
138             if (selStart == selEnd && selEnd > 0 &&
139                     content.charAt(selStart - 1) == '0') {
140                 content.replace(selStart - 1, selEnd, String.valueOf('+'));
141                 adjustMetaAfterKeypress(content);
142                 return true;
143             }
144         }
145 
146         adjustMetaAfterKeypress(content);
147         return super.onKeyDown(view, content, keyCode, event);
148     }
149 
150     /* package */
151     @Nullable
addDigits(@onNull Collection<Character> collection, @Nullable Locale locale)152     static boolean addDigits(@NonNull Collection<Character> collection, @Nullable Locale locale) {
153         if (locale == null) {
154             return false;
155         }
156         final String[] digits = DecimalFormatSymbols.getInstance(locale).getDigitStrings();
157         for (int i = 0; i < 10; i++) {
158             if (digits[i].length() > 1) { // multi-codeunit digits. Not supported.
159                 return false;
160             }
161             collection.add(Character.valueOf(digits[i].charAt(0)));
162         }
163         return true;
164     }
165 
166     // From http://unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns
167     private static final String DATE_TIME_FORMAT_SYMBOLS =
168             "GyYuUrQqMLlwWdDFgEecabBhHKkjJCmsSAzZOvVXx";
169     private static final char SINGLE_QUOTE = '\'';
170 
171     /* package */
addFormatCharsFromSkeleton( @onNull Collection<Character> collection, @Nullable Locale locale, @NonNull String skeleton, @NonNull String symbolsToIgnore)172     static boolean addFormatCharsFromSkeleton(
173             @NonNull Collection<Character> collection, @Nullable Locale locale,
174             @NonNull String skeleton, @NonNull String symbolsToIgnore) {
175         if (locale == null) {
176             return false;
177         }
178         final String pattern = DateFormat.getBestDateTimePattern(locale, skeleton);
179         boolean outsideQuotes = true;
180         for (int i = 0; i < pattern.length(); i++) {
181             final char ch = pattern.charAt(i);
182             if (Character.isSurrogate(ch)) { // characters outside BMP are not supported.
183                 return false;
184             } else if (ch == SINGLE_QUOTE) {
185                 outsideQuotes = !outsideQuotes;
186                 // Single quote characters should be considered if and only if they follow
187                 // another single quote.
188                 if (i == 0 || pattern.charAt(i - 1) != SINGLE_QUOTE) {
189                     continue;
190                 }
191             }
192 
193             if (outsideQuotes) {
194                 if (symbolsToIgnore.indexOf(ch) != -1) {
195                     // Skip expected pattern characters.
196                     continue;
197                 } else if (DATE_TIME_FORMAT_SYMBOLS.indexOf(ch) != -1) {
198                     // An unexpected symbols is seen. We've failed.
199                     return false;
200                 }
201             }
202             // If we are here, we are either inside quotes, or we have seen a non-pattern
203             // character outside quotes. So ch is a valid character in a date.
204             collection.add(Character.valueOf(ch));
205         }
206         return true;
207     }
208 
209     /* package */
addFormatCharsFromSkeletons( @onNull Collection<Character> collection, @Nullable Locale locale, @NonNull String[] skeletons, @NonNull String symbolsToIgnore)210     static boolean addFormatCharsFromSkeletons(
211             @NonNull Collection<Character> collection, @Nullable Locale locale,
212             @NonNull String[] skeletons, @NonNull String symbolsToIgnore) {
213         for (int i = 0; i < skeletons.length; i++) {
214             final boolean success = addFormatCharsFromSkeleton(
215                     collection, locale, skeletons[i], symbolsToIgnore);
216             if (!success) {
217                 return false;
218             }
219         }
220         return true;
221     }
222 
223 
224     /* package */
addAmPmChars(@onNull Collection<Character> collection, @Nullable Locale locale)225     static boolean addAmPmChars(@NonNull Collection<Character> collection,
226                                 @Nullable Locale locale) {
227         if (locale == null) {
228             return false;
229         }
230         final String[] amPm = DateFormat.getIcuDateFormatSymbols(locale).getAmPmStrings();
231         for (int i = 0; i < amPm.length; i++) {
232             for (int j = 0; j < amPm[i].length(); j++) {
233                 final char ch = amPm[i].charAt(j);
234                 if (Character.isBmpCodePoint(ch)) {
235                     collection.add(Character.valueOf(ch));
236                 } else {  // We don't support non-BMP characters.
237                     return false;
238                 }
239             }
240         }
241         return true;
242     }
243 
244     /* package */
245     @NonNull
collectionToArray(@onNull Collection<Character> chars)246     static char[] collectionToArray(@NonNull Collection<Character> chars) {
247         final char[] result = new char[chars.size()];
248         int i = 0;
249         for (Character ch : chars) {
250             result[i++] = ch;
251         }
252         return result;
253     }
254 }
255