/*
|
* Copyright (C) 2006 The Android Open Source Project
|
*
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
* you may not use this file except in compliance with the License.
|
* You may obtain a copy of the License at
|
*
|
* http://www.apache.org/licenses/LICENSE-2.0
|
*
|
* Unless required by applicable law or agreed to in writing, software
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* See the License for the specific language governing permissions and
|
* limitations under the License.
|
*/
|
|
package android.text.method;
|
|
import android.annotation.NonNull;
|
import android.annotation.Nullable;
|
import android.icu.lang.UCharacter;
|
import android.icu.lang.UProperty;
|
import android.icu.text.DecimalFormatSymbols;
|
import android.text.InputType;
|
import android.text.SpannableStringBuilder;
|
import android.text.Spanned;
|
import android.view.KeyEvent;
|
|
import com.android.internal.annotations.GuardedBy;
|
import com.android.internal.util.ArrayUtils;
|
|
import java.util.HashMap;
|
import java.util.LinkedHashSet;
|
import java.util.Locale;
|
|
/**
|
* For digits-only text entry
|
* <p></p>
|
* As for all implementations of {@link KeyListener}, this class is only concerned
|
* with hardware keyboards. Software input methods have no obligation to trigger
|
* the methods in this class.
|
*/
|
public class DigitsKeyListener extends NumberKeyListener
|
{
|
private char[] mAccepted;
|
private boolean mNeedsAdvancedInput;
|
private final boolean mSign;
|
private final boolean mDecimal;
|
private final boolean mStringMode;
|
@Nullable
|
private final Locale mLocale;
|
|
private static final String DEFAULT_DECIMAL_POINT_CHARS = ".";
|
private static final String DEFAULT_SIGN_CHARS = "-+";
|
|
private static final char HYPHEN_MINUS = '-';
|
// Various locales use this as minus sign
|
private static final char MINUS_SIGN = '\u2212';
|
// Slovenian uses this as minus sign (a bug?): http://unicode.org/cldr/trac/ticket/10050
|
private static final char EN_DASH = '\u2013';
|
|
private String mDecimalPointChars = DEFAULT_DECIMAL_POINT_CHARS;
|
private String mSignChars = DEFAULT_SIGN_CHARS;
|
|
private static final int SIGN = 1;
|
private static final int DECIMAL = 2;
|
|
@Override
|
protected char[] getAcceptedChars() {
|
return mAccepted;
|
}
|
|
/**
|
* The characters that are used in compatibility mode.
|
*
|
* @see KeyEvent#getMatch
|
* @see #getAcceptedChars
|
*/
|
private static final char[][] COMPATIBILITY_CHARACTERS = {
|
{ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' },
|
{ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '+' },
|
{ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.' },
|
{ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '+', '.' },
|
};
|
|
private boolean isSignChar(final char c) {
|
return mSignChars.indexOf(c) != -1;
|
}
|
|
private boolean isDecimalPointChar(final char c) {
|
return mDecimalPointChars.indexOf(c) != -1;
|
}
|
|
/**
|
* Allocates a DigitsKeyListener that accepts the ASCII digits 0 through 9.
|
*
|
* @deprecated Use {@link #DigitsKeyListener(Locale)} instead.
|
*/
|
@Deprecated
|
public DigitsKeyListener() {
|
this(null, false, false);
|
}
|
|
/**
|
* Allocates a DigitsKeyListener that accepts the ASCII digits 0 through 9, plus the ASCII plus
|
* or minus sign (only at the beginning) and/or the ASCII period ('.') as the decimal point
|
* (only one per field) if specified.
|
*
|
* @deprecated Use {@link #DigitsKeyListener(Locale, boolean, boolean)} instead.
|
*/
|
@Deprecated
|
public DigitsKeyListener(boolean sign, boolean decimal) {
|
this(null, sign, decimal);
|
}
|
|
public DigitsKeyListener(@Nullable Locale locale) {
|
this(locale, false, false);
|
}
|
|
private void setToCompat() {
|
mDecimalPointChars = DEFAULT_DECIMAL_POINT_CHARS;
|
mSignChars = DEFAULT_SIGN_CHARS;
|
final int kind = (mSign ? SIGN : 0) | (mDecimal ? DECIMAL : 0);
|
mAccepted = COMPATIBILITY_CHARACTERS[kind];
|
mNeedsAdvancedInput = false;
|
}
|
|
private void calculateNeedForAdvancedInput() {
|
final int kind = (mSign ? SIGN : 0) | (mDecimal ? DECIMAL : 0);
|
mNeedsAdvancedInput = !ArrayUtils.containsAll(COMPATIBILITY_CHARACTERS[kind], mAccepted);
|
}
|
|
// Takes a sign string and strips off its bidi controls, if any.
|
@NonNull
|
private static String stripBidiControls(@NonNull String sign) {
|
// For the sake of simplicity, we operate on code units, since all bidi controls are
|
// in the BMP. We also expect the string to be very short (almost always 1 character), so we
|
// don't need to use StringBuilder.
|
String result = "";
|
for (int i = 0; i < sign.length(); i++) {
|
final char c = sign.charAt(i);
|
if (!UCharacter.hasBinaryProperty(c, UProperty.BIDI_CONTROL)) {
|
if (result.isEmpty()) {
|
result = String.valueOf(c);
|
} else {
|
// This should happen very rarely, only if we have a multi-character sign,
|
// or a sign outside BMP.
|
result += c;
|
}
|
}
|
}
|
return result;
|
}
|
|
public DigitsKeyListener(@Nullable Locale locale, boolean sign, boolean decimal) {
|
mSign = sign;
|
mDecimal = decimal;
|
mStringMode = false;
|
mLocale = locale;
|
if (locale == null) {
|
setToCompat();
|
return;
|
}
|
LinkedHashSet<Character> chars = new LinkedHashSet<>();
|
final boolean success = NumberKeyListener.addDigits(chars, locale);
|
if (!success) {
|
setToCompat();
|
return;
|
}
|
if (sign || decimal) {
|
final DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(locale);
|
if (sign) {
|
final String minusString = stripBidiControls(symbols.getMinusSignString());
|
final String plusString = stripBidiControls(symbols.getPlusSignString());
|
if (minusString.length() > 1 || plusString.length() > 1) {
|
// non-BMP and multi-character signs are not supported.
|
setToCompat();
|
return;
|
}
|
final char minus = minusString.charAt(0);
|
final char plus = plusString.charAt(0);
|
chars.add(Character.valueOf(minus));
|
chars.add(Character.valueOf(plus));
|
mSignChars = "" + minus + plus;
|
|
if (minus == MINUS_SIGN || minus == EN_DASH) {
|
// If the minus sign is U+2212 MINUS SIGN or U+2013 EN DASH, we also need to
|
// accept the ASCII hyphen-minus.
|
chars.add(HYPHEN_MINUS);
|
mSignChars += HYPHEN_MINUS;
|
}
|
}
|
if (decimal) {
|
final String separatorString = symbols.getDecimalSeparatorString();
|
if (separatorString.length() > 1) {
|
// non-BMP and multi-character decimal separators are not supported.
|
setToCompat();
|
return;
|
}
|
final Character separatorChar = Character.valueOf(separatorString.charAt(0));
|
chars.add(separatorChar);
|
mDecimalPointChars = separatorChar.toString();
|
}
|
}
|
mAccepted = NumberKeyListener.collectionToArray(chars);
|
calculateNeedForAdvancedInput();
|
}
|
|
private DigitsKeyListener(@NonNull final String accepted) {
|
mSign = false;
|
mDecimal = false;
|
mStringMode = true;
|
mLocale = null;
|
mAccepted = new char[accepted.length()];
|
accepted.getChars(0, accepted.length(), mAccepted, 0);
|
// Theoretically we may need advanced input, but for backward compatibility, we don't change
|
// the input type.
|
mNeedsAdvancedInput = false;
|
}
|
|
/**
|
* Returns a DigitsKeyListener that accepts the ASCII digits 0 through 9.
|
*
|
* @deprecated Use {@link #getInstance(Locale)} instead.
|
*/
|
@Deprecated
|
@NonNull
|
public static DigitsKeyListener getInstance() {
|
return getInstance(false, false);
|
}
|
|
/**
|
* Returns a DigitsKeyListener that accepts the ASCII digits 0 through 9, plus the ASCII plus
|
* or minus sign (only at the beginning) and/or the ASCII period ('.') as the decimal point
|
* (only one per field) if specified.
|
*
|
* @deprecated Use {@link #getInstance(Locale, boolean, boolean)} instead.
|
*/
|
@Deprecated
|
@NonNull
|
public static DigitsKeyListener getInstance(boolean sign, boolean decimal) {
|
return getInstance(null, sign, decimal);
|
}
|
|
/**
|
* Returns a DigitsKeyListener that accepts the locale-appropriate digits.
|
*/
|
@NonNull
|
public static DigitsKeyListener getInstance(@Nullable Locale locale) {
|
return getInstance(locale, false, false);
|
}
|
|
private static final Object sLocaleCacheLock = new Object();
|
@GuardedBy("sLocaleCacheLock")
|
private static final HashMap<Locale, DigitsKeyListener[]> sLocaleInstanceCache =
|
new HashMap<>();
|
|
/**
|
* Returns a DigitsKeyListener that accepts the locale-appropriate digits, plus the
|
* locale-appropriate plus or minus sign (only at the beginning) and/or the locale-appropriate
|
* decimal separator (only one per field) if specified.
|
*/
|
@NonNull
|
public static DigitsKeyListener getInstance(
|
@Nullable Locale locale, boolean sign, boolean decimal) {
|
final int kind = (sign ? SIGN : 0) | (decimal ? DECIMAL : 0);
|
synchronized (sLocaleCacheLock) {
|
DigitsKeyListener[] cachedValue = sLocaleInstanceCache.get(locale);
|
if (cachedValue != null && cachedValue[kind] != null) {
|
return cachedValue[kind];
|
}
|
if (cachedValue == null) {
|
cachedValue = new DigitsKeyListener[4];
|
sLocaleInstanceCache.put(locale, cachedValue);
|
}
|
return cachedValue[kind] = new DigitsKeyListener(locale, sign, decimal);
|
}
|
}
|
|
private static final Object sStringCacheLock = new Object();
|
@GuardedBy("sStringCacheLock")
|
private static final HashMap<String, DigitsKeyListener> sStringInstanceCache = new HashMap<>();
|
|
/**
|
* Returns a DigitsKeyListener that accepts only the characters
|
* that appear in the specified String. Note that not all characters
|
* may be available on every keyboard.
|
*/
|
@NonNull
|
public static DigitsKeyListener getInstance(@NonNull String accepted) {
|
DigitsKeyListener result;
|
synchronized (sStringCacheLock) {
|
result = sStringInstanceCache.get(accepted);
|
if (result == null) {
|
result = new DigitsKeyListener(accepted);
|
sStringInstanceCache.put(accepted, result);
|
}
|
}
|
return result;
|
}
|
|
/**
|
* Returns a DigitsKeyListener based on an the settings of a existing DigitsKeyListener, with
|
* the locale modified.
|
*
|
* @hide
|
*/
|
@NonNull
|
public static DigitsKeyListener getInstance(
|
@Nullable Locale locale,
|
@NonNull DigitsKeyListener listener) {
|
if (listener.mStringMode) {
|
return listener; // string-mode DigitsKeyListeners have no locale.
|
} else {
|
return getInstance(locale, listener.mSign, listener.mDecimal);
|
}
|
}
|
|
/**
|
* Returns the input type for the listener.
|
*/
|
public int getInputType() {
|
int contentType;
|
if (mNeedsAdvancedInput) {
|
contentType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_NORMAL;
|
} else {
|
contentType = InputType.TYPE_CLASS_NUMBER;
|
if (mSign) {
|
contentType |= InputType.TYPE_NUMBER_FLAG_SIGNED;
|
}
|
if (mDecimal) {
|
contentType |= InputType.TYPE_NUMBER_FLAG_DECIMAL;
|
}
|
}
|
return contentType;
|
}
|
|
@Override
|
public CharSequence filter(CharSequence source, int start, int end,
|
Spanned dest, int dstart, int dend) {
|
CharSequence out = super.filter(source, start, end, dest, dstart, dend);
|
|
if (mSign == false && mDecimal == false) {
|
return out;
|
}
|
|
if (out != null) {
|
source = out;
|
start = 0;
|
end = out.length();
|
}
|
|
int sign = -1;
|
int decimal = -1;
|
int dlen = dest.length();
|
|
/*
|
* Find out if the existing text has a sign or decimal point characters.
|
*/
|
|
for (int i = 0; i < dstart; i++) {
|
char c = dest.charAt(i);
|
|
if (isSignChar(c)) {
|
sign = i;
|
} else if (isDecimalPointChar(c)) {
|
decimal = i;
|
}
|
}
|
for (int i = dend; i < dlen; i++) {
|
char c = dest.charAt(i);
|
|
if (isSignChar(c)) {
|
return ""; // Nothing can be inserted in front of a sign character.
|
} else if (isDecimalPointChar(c)) {
|
decimal = i;
|
}
|
}
|
|
/*
|
* If it does, we must strip them out from the source.
|
* In addition, a sign character must be the very first character,
|
* and nothing can be inserted before an existing sign character.
|
* Go in reverse order so the offsets are stable.
|
*/
|
|
SpannableStringBuilder stripped = null;
|
|
for (int i = end - 1; i >= start; i--) {
|
char c = source.charAt(i);
|
boolean strip = false;
|
|
if (isSignChar(c)) {
|
if (i != start || dstart != 0) {
|
strip = true;
|
} else if (sign >= 0) {
|
strip = true;
|
} else {
|
sign = i;
|
}
|
} else if (isDecimalPointChar(c)) {
|
if (decimal >= 0) {
|
strip = true;
|
} else {
|
decimal = i;
|
}
|
}
|
|
if (strip) {
|
if (end == start + 1) {
|
return ""; // Only one character, and it was stripped.
|
}
|
|
if (stripped == null) {
|
stripped = new SpannableStringBuilder(source, start, end);
|
}
|
|
stripped.delete(i - start, i + 1 - start);
|
}
|
}
|
|
if (stripped != null) {
|
return stripped;
|
} else if (out != null) {
|
return out;
|
} else {
|
return null;
|
}
|
}
|
}
|