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