DigitsKeyListener.java revision 3484ba8fdc8f5c91937af23e6d59025081c02367
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.lang.UCharacter;
22import android.icu.lang.UProperty;
23import android.icu.text.DecimalFormatSymbols;
24import android.text.InputType;
25import android.text.SpannableStringBuilder;
26import android.text.Spanned;
27import android.view.KeyEvent;
28
29import com.android.internal.annotations.GuardedBy;
30
31import java.util.HashMap;
32import java.util.LinkedHashSet;
33import java.util.Locale;
34
35/**
36 * For digits-only 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 */
42public class DigitsKeyListener extends NumberKeyListener
43{
44    private char[] mAccepted;
45    private final boolean mSign;
46    private final boolean mDecimal;
47
48    private static final String DEFAULT_DECIMAL_POINT_CHARS = ".";
49    private static final String DEFAULT_SIGN_CHARS = "-+";
50
51    private static final char HYPHEN_MINUS = '-';
52    // Various locales use this as minus sign
53    private static final char MINUS_SIGN = '\u2212';
54    // Slovenian uses this as minus sign (a bug?): http://unicode.org/cldr/trac/ticket/10050
55    private static final char EN_DASH = '\u2013';
56
57    private String mDecimalPointChars = DEFAULT_DECIMAL_POINT_CHARS;
58    private String mSignChars = DEFAULT_SIGN_CHARS;
59
60    private static final int SIGN = 1;
61    private static final int DECIMAL = 2;
62
63    @Override
64    protected char[] getAcceptedChars() {
65        return mAccepted;
66    }
67
68    /**
69     * The characters that are used in compatibility mode.
70     *
71     * @see KeyEvent#getMatch
72     * @see #getAcceptedChars
73     */
74    private static final char[][] COMPATIBILITY_CHARACTERS = {
75        { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' },
76        { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '+' },
77        { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.' },
78        { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '+', '.' },
79    };
80
81    private boolean isSignChar(final char c) {
82        return mSignChars.indexOf(c) != -1;
83    }
84
85    private boolean isDecimalPointChar(final char c) {
86        return mDecimalPointChars.indexOf(c) != -1;
87    }
88
89    /**
90     * Allocates a DigitsKeyListener that accepts the ASCII digits 0 through 9.
91     *
92     * @deprecated Use {@link #DigitsKeyListener(Locale)} instead.
93     */
94    @Deprecated
95    public DigitsKeyListener() {
96        this(null, false, false);
97    }
98
99    /**
100     * Allocates a DigitsKeyListener that accepts the ASCII digits 0 through 9, plus the ASCII plus
101     * or minus sign (only at the beginning) and/or the ASCII period ('.') as the decimal point
102     * (only one per field) if specified.
103     *
104     * @deprecated Use {@link #DigitsKeyListener(Locale, boolean, boolean)} instead.
105     */
106    @Deprecated
107    public DigitsKeyListener(boolean sign, boolean decimal) {
108        this(null, sign, decimal);
109    }
110
111    public DigitsKeyListener(@Nullable Locale locale) {
112        this(locale, false, false);
113    }
114
115    private void setToCompat(boolean sign, boolean decimal) {
116        mDecimalPointChars = DEFAULT_DECIMAL_POINT_CHARS;
117        mSignChars = DEFAULT_SIGN_CHARS;
118        final int kind = (sign ? SIGN : 0) | (decimal ? DECIMAL : 0);
119        mAccepted = COMPATIBILITY_CHARACTERS[kind];
120    }
121
122    // Takes a sign string and strips off its bidi controls, if any.
123    @NonNull
124    private static String stripBidiControls(@NonNull String sign) {
125        // For the sake of simplicity, we operate on code units, since all bidi controls are
126        // in the BMP. We also expect the string to be very short (almost always 1 character), so we
127        // don't need to use StringBuilder.
128        String result = "";
129        for (int i = 0; i < sign.length(); i++) {
130            final char c = sign.charAt(i);
131            if (!UCharacter.hasBinaryProperty(c, UProperty.BIDI_CONTROL)) {
132                if (result.isEmpty()) {
133                    result = String.valueOf(c);
134                } else {
135                    // This should happen very rarely, only if we have a multi-character sign,
136                    // or a sign outside BMP.
137                    result += c;
138                }
139            }
140        }
141        return result;
142    }
143
144    public DigitsKeyListener(@Nullable Locale locale, boolean sign, boolean decimal) {
145        mSign = sign;
146        mDecimal = decimal;
147        if (locale == null) {
148            setToCompat(sign, decimal);
149            return;
150        }
151        LinkedHashSet<Character> chars = new LinkedHashSet<>();
152        final boolean success = NumberKeyListener.addDigits(chars, locale);
153        if (!success) {
154            setToCompat(sign, decimal);
155            return;
156        }
157        if (sign || decimal) {
158            final DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(locale);
159            if (sign) {
160                final String minusString = stripBidiControls(symbols.getMinusSignString());
161                final String plusString = stripBidiControls(symbols.getPlusSignString());
162                if (minusString.length() > 1 || plusString.length() > 1) {
163                    // non-BMP and multi-character signs are not supported.
164                    setToCompat(sign, decimal);
165                    return;
166                }
167                final char minus = minusString.charAt(0);
168                final char plus = plusString.charAt(0);
169                chars.add(Character.valueOf(minus));
170                chars.add(Character.valueOf(plus));
171                mSignChars = "" + minus + plus;
172
173                if (minus == MINUS_SIGN || minus == EN_DASH) {
174                    // If the minus sign is U+2212 MINUS SIGN or U+2013 EN DASH, we also need to
175                    // accept the ASCII hyphen-minus.
176                    chars.add(HYPHEN_MINUS);
177                    mSignChars += HYPHEN_MINUS;
178                }
179            }
180            if (decimal) {
181                final String separatorString = symbols.getDecimalSeparatorString();
182                if (separatorString.length() > 1) {
183                    // non-BMP and multi-character decimal separators are not supported.
184                    setToCompat(sign, decimal);
185                    return;
186                }
187                final Character separatorChar = Character.valueOf(separatorString.charAt(0));
188                chars.add(separatorChar);
189                mDecimalPointChars = separatorChar.toString();
190            }
191        }
192        mAccepted = NumberKeyListener.collectionToArray(chars);
193    }
194
195    private DigitsKeyListener(@NonNull final String accepted) {
196        mSign = false;
197        mDecimal = false;
198        mAccepted = new char[accepted.length()];
199        accepted.getChars(0, accepted.length(), mAccepted, 0);
200    }
201
202    /**
203     * Returns a DigitsKeyListener that accepts the ASCII digits 0 through 9.
204     *
205     * @deprecated Use {@link #getInstance(Locale)} instead.
206     */
207    @Deprecated
208    @NonNull
209    public static DigitsKeyListener getInstance() {
210        return getInstance(false, false);
211    }
212
213    /**
214     * Returns a DigitsKeyListener that accepts the ASCII digits 0 through 9, plus the ASCII plus
215     * or minus sign (only at the beginning) and/or the ASCII period ('.') as the decimal point
216     * (only one per field) if specified.
217     *
218     * @deprecated Use {@link #getInstance(Locale, boolean, boolean)} instead.
219     */
220    @Deprecated
221    @NonNull
222    public static DigitsKeyListener getInstance(boolean sign, boolean decimal) {
223        return getInstance(null, sign, decimal);
224    }
225
226    /**
227     * Returns a DigitsKeyListener that accepts the locale-appropriate digits.
228     */
229    @NonNull
230    public static DigitsKeyListener getInstance(@Nullable Locale locale) {
231        return getInstance(locale, false, false);
232    }
233
234    private static final Object sLocaleCacheLock = new Object();
235    @GuardedBy("sLocaleCacheLock")
236    private static final HashMap<Locale, DigitsKeyListener[]> sLocaleInstanceCache =
237            new HashMap<>();
238
239    /**
240     * Returns a DigitsKeyListener that accepts the locale-appropriate digits, plus the
241     * locale-appropriate plus or minus sign (only at the beginning) and/or the locale-appropriate
242     * decimal separator (only one per field) if specified.
243     */
244    @NonNull
245    public static DigitsKeyListener getInstance(
246            @Nullable Locale locale, boolean sign, boolean decimal) {
247        final int kind = (sign ? SIGN : 0) | (decimal ? DECIMAL : 0);
248        synchronized (sLocaleCacheLock) {
249            DigitsKeyListener[] cachedValue = sLocaleInstanceCache.get(locale);
250            if (cachedValue != null && cachedValue[kind] != null) {
251                return cachedValue[kind];
252            }
253            if (cachedValue == null) {
254                cachedValue = new DigitsKeyListener[4];
255                sLocaleInstanceCache.put(locale, cachedValue);
256            }
257            return cachedValue[kind] = new DigitsKeyListener(locale, sign, decimal);
258        }
259    }
260
261    private static final Object sStringCacheLock = new Object();
262    @GuardedBy("sStringCacheLock")
263    private static final HashMap<String, DigitsKeyListener> sStringInstanceCache = new HashMap<>();
264
265    /**
266     * Returns a DigitsKeyListener that accepts only the characters
267     * that appear in the specified String.  Note that not all characters
268     * may be available on every keyboard.
269     */
270    @NonNull
271    public static DigitsKeyListener getInstance(@NonNull String accepted) {
272        DigitsKeyListener result;
273        synchronized (sStringCacheLock) {
274            result = sStringInstanceCache.get(accepted);
275            if (result == null) {
276                result = new DigitsKeyListener(accepted);
277                sStringInstanceCache.put(accepted, result);
278            }
279        }
280        return result;
281    }
282
283    public int getInputType() {
284        int contentType = InputType.TYPE_CLASS_NUMBER;
285        if (mSign) {
286            contentType |= InputType.TYPE_NUMBER_FLAG_SIGNED;
287        }
288        if (mDecimal) {
289            contentType |= InputType.TYPE_NUMBER_FLAG_DECIMAL;
290        }
291        return contentType;
292    }
293
294    @Override
295    public CharSequence filter(CharSequence source, int start, int end,
296                               Spanned dest, int dstart, int dend) {
297        CharSequence out = super.filter(source, start, end, dest, dstart, dend);
298
299        if (mSign == false && mDecimal == false) {
300            return out;
301        }
302
303        if (out != null) {
304            source = out;
305            start = 0;
306            end = out.length();
307        }
308
309        int sign = -1;
310        int decimal = -1;
311        int dlen = dest.length();
312
313        /*
314         * Find out if the existing text has a sign or decimal point characters.
315         */
316
317        for (int i = 0; i < dstart; i++) {
318            char c = dest.charAt(i);
319
320            if (isSignChar(c)) {
321                sign = i;
322            } else if (isDecimalPointChar(c)) {
323                decimal = i;
324            }
325        }
326        for (int i = dend; i < dlen; i++) {
327            char c = dest.charAt(i);
328
329            if (isSignChar(c)) {
330                return "";    // Nothing can be inserted in front of a sign character.
331            } else if (isDecimalPointChar(c)) {
332                decimal = i;
333            }
334        }
335
336        /*
337         * If it does, we must strip them out from the source.
338         * In addition, a sign character must be the very first character,
339         * and nothing can be inserted before an existing sign character.
340         * Go in reverse order so the offsets are stable.
341         */
342
343        SpannableStringBuilder stripped = null;
344
345        for (int i = end - 1; i >= start; i--) {
346            char c = source.charAt(i);
347            boolean strip = false;
348
349            if (isSignChar(c)) {
350                if (i != start || dstart != 0) {
351                    strip = true;
352                } else if (sign >= 0) {
353                    strip = true;
354                } else {
355                    sign = i;
356                }
357            } else if (isDecimalPointChar(c)) {
358                if (decimal >= 0) {
359                    strip = true;
360                } else {
361                    decimal = i;
362                }
363            }
364
365            if (strip) {
366                if (end == start + 1) {
367                    return "";  // Only one character, and it was stripped.
368                }
369
370                if (stripped == null) {
371                    stripped = new SpannableStringBuilder(source, start, end);
372                }
373
374                stripped.delete(i - start, i + 1 - start);
375            }
376        }
377
378        if (stripped != null) {
379            return stripped;
380        } else if (out != null) {
381            return out;
382        } else {
383            return null;
384        }
385    }
386}
387