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