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.text.InputType;
20import android.text.Spanned;
21import android.text.SpannableStringBuilder;
22import android.view.KeyEvent;
23
24
25/**
26 * For digits-only text entry
27 * <p></p>
28 * As for all implementations of {@link KeyListener}, this class is only concerned
29 * with hardware keyboards.  Software input methods have no obligation to trigger
30 * the methods in this class.
31 */
32public class DigitsKeyListener extends NumberKeyListener
33{
34    private char[] mAccepted;
35    private boolean mSign;
36    private boolean mDecimal;
37
38    private static final int SIGN = 1;
39    private static final int DECIMAL = 2;
40
41    @Override
42    protected char[] getAcceptedChars() {
43        return mAccepted;
44    }
45
46    /**
47     * The characters that are used.
48     *
49     * @see KeyEvent#getMatch
50     * @see #getAcceptedChars
51     */
52    private static final char[][] CHARACTERS = {
53        { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' },
54        { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '+' },
55        { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.' },
56        { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '+', '.' },
57    };
58
59    private static boolean isSignChar(final char c) {
60        return c == '-' || c == '+';
61    }
62
63    // TODO: Needs internationalization
64    private static boolean isDecimalPointChar(final char c) {
65        return c == '.';
66    }
67
68    /**
69     * Allocates a DigitsKeyListener that accepts the digits 0 through 9.
70     */
71    public DigitsKeyListener() {
72        this(false, false);
73    }
74
75    /**
76     * Allocates a DigitsKeyListener that accepts the digits 0 through 9,
77     * plus the minus sign (only at the beginning) and/or decimal point
78     * (only one per field) if specified.
79     */
80    public DigitsKeyListener(boolean sign, boolean decimal) {
81        mSign = sign;
82        mDecimal = decimal;
83
84        int kind = (sign ? SIGN : 0) | (decimal ? DECIMAL : 0);
85        mAccepted = CHARACTERS[kind];
86    }
87
88    /**
89     * Returns a DigitsKeyListener that accepts the digits 0 through 9.
90     */
91    public static DigitsKeyListener getInstance() {
92        return getInstance(false, false);
93    }
94
95    /**
96     * Returns a DigitsKeyListener that accepts the digits 0 through 9,
97     * plus the minus sign (only at the beginning) and/or decimal point
98     * (only one per field) if specified.
99     */
100    public static DigitsKeyListener getInstance(boolean sign, boolean decimal) {
101        int kind = (sign ? SIGN : 0) | (decimal ? DECIMAL : 0);
102
103        if (sInstance[kind] != null)
104            return sInstance[kind];
105
106        sInstance[kind] = new DigitsKeyListener(sign, decimal);
107        return sInstance[kind];
108    }
109
110    /**
111     * Returns a DigitsKeyListener that accepts only the characters
112     * that appear in the specified String.  Note that not all characters
113     * may be available on every keyboard.
114     */
115    public static DigitsKeyListener getInstance(String accepted) {
116        // TODO: do we need a cache of these to avoid allocating?
117
118        DigitsKeyListener dim = new DigitsKeyListener();
119
120        dim.mAccepted = new char[accepted.length()];
121        accepted.getChars(0, accepted.length(), dim.mAccepted, 0);
122
123        return dim;
124    }
125
126    public int getInputType() {
127        int contentType = InputType.TYPE_CLASS_NUMBER;
128        if (mSign) {
129            contentType |= InputType.TYPE_NUMBER_FLAG_SIGNED;
130        }
131        if (mDecimal) {
132            contentType |= InputType.TYPE_NUMBER_FLAG_DECIMAL;
133        }
134        return contentType;
135    }
136
137    @Override
138    public CharSequence filter(CharSequence source, int start, int end,
139                               Spanned dest, int dstart, int dend) {
140        CharSequence out = super.filter(source, start, end, dest, dstart, dend);
141
142        if (mSign == false && mDecimal == false) {
143            return out;
144        }
145
146        if (out != null) {
147            source = out;
148            start = 0;
149            end = out.length();
150        }
151
152        int sign = -1;
153        int decimal = -1;
154        int dlen = dest.length();
155
156        /*
157         * Find out if the existing text has a sign or decimal point characters.
158         */
159
160        for (int i = 0; i < dstart; i++) {
161            char c = dest.charAt(i);
162
163            if (isSignChar(c)) {
164                sign = i;
165            } else if (isDecimalPointChar(c)) {
166                decimal = i;
167            }
168        }
169        for (int i = dend; i < dlen; i++) {
170            char c = dest.charAt(i);
171
172            if (isSignChar(c)) {
173                return "";    // Nothing can be inserted in front of a sign character.
174            } else if (isDecimalPointChar(c)) {
175                decimal = i;
176            }
177        }
178
179        /*
180         * If it does, we must strip them out from the source.
181         * In addition, a sign character must be the very first character,
182         * and nothing can be inserted before an existing sign character.
183         * Go in reverse order so the offsets are stable.
184         */
185
186        SpannableStringBuilder stripped = null;
187
188        for (int i = end - 1; i >= start; i--) {
189            char c = source.charAt(i);
190            boolean strip = false;
191
192            if (isSignChar(c)) {
193                if (i != start || dstart != 0) {
194                    strip = true;
195                } else if (sign >= 0) {
196                    strip = true;
197                } else {
198                    sign = i;
199                }
200            } else if (isDecimalPointChar(c)) {
201                if (decimal >= 0) {
202                    strip = true;
203                } else {
204                    decimal = i;
205                }
206            }
207
208            if (strip) {
209                if (end == start + 1) {
210                    return "";  // Only one character, and it was stripped.
211                }
212
213                if (stripped == null) {
214                    stripped = new SpannableStringBuilder(source, start, end);
215                }
216
217                stripped.delete(i - start, i + 1 - start);
218            }
219        }
220
221        if (stripped != null) {
222            return stripped;
223        } else if (out != null) {
224            return out;
225        } else {
226            return null;
227        }
228    }
229
230    private static DigitsKeyListener[] sInstance = new DigitsKeyListener[4];
231}
232