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