diff --git a/api/current.txt b/api/current.txt
index 6f406402d9ec3..d4811f0e54668 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -41017,19 +41017,23 @@ package android.text.method {
}
public class DateKeyListener extends android.text.method.NumberKeyListener {
- ctor public DateKeyListener();
+ ctor public deprecated DateKeyListener();
+ ctor public DateKeyListener(java.util.Locale);
method protected char[] getAcceptedChars();
method public int getInputType();
- method public static android.text.method.DateKeyListener getInstance();
- field public static final char[] CHARACTERS;
+ method public static deprecated android.text.method.DateKeyListener getInstance();
+ method public static android.text.method.DateKeyListener getInstance(java.util.Locale);
+ field public static final deprecated char[] CHARACTERS;
}
public class DateTimeKeyListener extends android.text.method.NumberKeyListener {
- ctor public DateTimeKeyListener();
+ ctor public deprecated DateTimeKeyListener();
+ ctor public DateTimeKeyListener(java.util.Locale);
method protected char[] getAcceptedChars();
method public int getInputType();
- method public static android.text.method.DateTimeKeyListener getInstance();
- field public static final char[] CHARACTERS;
+ method public static deprecated android.text.method.DateTimeKeyListener getInstance();
+ method public static android.text.method.DateTimeKeyListener getInstance(java.util.Locale);
+ field public static final deprecated char[] CHARACTERS;
}
public class DialerKeyListener extends android.text.method.NumberKeyListener {
@@ -41041,12 +41045,16 @@ package android.text.method {
}
public class DigitsKeyListener extends android.text.method.NumberKeyListener {
- ctor public DigitsKeyListener();
- ctor public DigitsKeyListener(boolean, boolean);
+ ctor public deprecated DigitsKeyListener();
+ ctor public deprecated DigitsKeyListener(boolean, boolean);
+ ctor public DigitsKeyListener(java.util.Locale);
+ ctor public DigitsKeyListener(java.util.Locale, boolean, boolean);
method protected char[] getAcceptedChars();
method public int getInputType();
- method public static android.text.method.DigitsKeyListener getInstance();
- method public static android.text.method.DigitsKeyListener getInstance(boolean, boolean);
+ method public static deprecated android.text.method.DigitsKeyListener getInstance();
+ method public static deprecated android.text.method.DigitsKeyListener getInstance(boolean, boolean);
+ method public static android.text.method.DigitsKeyListener getInstance(java.util.Locale);
+ method public static android.text.method.DigitsKeyListener getInstance(java.util.Locale, boolean, boolean);
method public static android.text.method.DigitsKeyListener getInstance(java.lang.String);
}
@@ -41190,11 +41198,13 @@ package android.text.method {
}
public class TimeKeyListener extends android.text.method.NumberKeyListener {
- ctor public TimeKeyListener();
+ ctor public deprecated TimeKeyListener();
+ ctor public TimeKeyListener(java.util.Locale);
method protected char[] getAcceptedChars();
method public int getInputType();
- method public static android.text.method.TimeKeyListener getInstance();
- field public static final char[] CHARACTERS;
+ method public static deprecated android.text.method.TimeKeyListener getInstance();
+ method public static android.text.method.TimeKeyListener getInstance(java.util.Locale);
+ field public static final deprecated char[] CHARACTERS;
}
public class Touch {
diff --git a/api/system-current.txt b/api/system-current.txt
index a9be2f7cfd3ab..781134d1dac5b 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -44452,19 +44452,23 @@ package android.text.method {
}
public class DateKeyListener extends android.text.method.NumberKeyListener {
- ctor public DateKeyListener();
+ ctor public deprecated DateKeyListener();
+ ctor public DateKeyListener(java.util.Locale);
method protected char[] getAcceptedChars();
method public int getInputType();
- method public static android.text.method.DateKeyListener getInstance();
- field public static final char[] CHARACTERS;
+ method public static deprecated android.text.method.DateKeyListener getInstance();
+ method public static android.text.method.DateKeyListener getInstance(java.util.Locale);
+ field public static final deprecated char[] CHARACTERS;
}
public class DateTimeKeyListener extends android.text.method.NumberKeyListener {
- ctor public DateTimeKeyListener();
+ ctor public deprecated DateTimeKeyListener();
+ ctor public DateTimeKeyListener(java.util.Locale);
method protected char[] getAcceptedChars();
method public int getInputType();
- method public static android.text.method.DateTimeKeyListener getInstance();
- field public static final char[] CHARACTERS;
+ method public static deprecated android.text.method.DateTimeKeyListener getInstance();
+ method public static android.text.method.DateTimeKeyListener getInstance(java.util.Locale);
+ field public static final deprecated char[] CHARACTERS;
}
public class DialerKeyListener extends android.text.method.NumberKeyListener {
@@ -44476,12 +44480,16 @@ package android.text.method {
}
public class DigitsKeyListener extends android.text.method.NumberKeyListener {
- ctor public DigitsKeyListener();
- ctor public DigitsKeyListener(boolean, boolean);
+ ctor public deprecated DigitsKeyListener();
+ ctor public deprecated DigitsKeyListener(boolean, boolean);
+ ctor public DigitsKeyListener(java.util.Locale);
+ ctor public DigitsKeyListener(java.util.Locale, boolean, boolean);
method protected char[] getAcceptedChars();
method public int getInputType();
- method public static android.text.method.DigitsKeyListener getInstance();
- method public static android.text.method.DigitsKeyListener getInstance(boolean, boolean);
+ method public static deprecated android.text.method.DigitsKeyListener getInstance();
+ method public static deprecated android.text.method.DigitsKeyListener getInstance(boolean, boolean);
+ method public static android.text.method.DigitsKeyListener getInstance(java.util.Locale);
+ method public static android.text.method.DigitsKeyListener getInstance(java.util.Locale, boolean, boolean);
method public static android.text.method.DigitsKeyListener getInstance(java.lang.String);
}
@@ -44625,11 +44633,13 @@ package android.text.method {
}
public class TimeKeyListener extends android.text.method.NumberKeyListener {
- ctor public TimeKeyListener();
+ ctor public deprecated TimeKeyListener();
+ ctor public TimeKeyListener(java.util.Locale);
method protected char[] getAcceptedChars();
method public int getInputType();
- method public static android.text.method.TimeKeyListener getInstance();
- field public static final char[] CHARACTERS;
+ method public static deprecated android.text.method.TimeKeyListener getInstance();
+ method public static android.text.method.TimeKeyListener getInstance(java.util.Locale);
+ field public static final deprecated char[] CHARACTERS;
}
public class Touch {
diff --git a/api/test-current.txt b/api/test-current.txt
index 5a423a5267891..79b95a011bf94 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -41156,19 +41156,23 @@ package android.text.method {
}
public class DateKeyListener extends android.text.method.NumberKeyListener {
- ctor public DateKeyListener();
+ ctor public deprecated DateKeyListener();
+ ctor public DateKeyListener(java.util.Locale);
method protected char[] getAcceptedChars();
method public int getInputType();
- method public static android.text.method.DateKeyListener getInstance();
- field public static final char[] CHARACTERS;
+ method public static deprecated android.text.method.DateKeyListener getInstance();
+ method public static android.text.method.DateKeyListener getInstance(java.util.Locale);
+ field public static final deprecated char[] CHARACTERS;
}
public class DateTimeKeyListener extends android.text.method.NumberKeyListener {
- ctor public DateTimeKeyListener();
+ ctor public deprecated DateTimeKeyListener();
+ ctor public DateTimeKeyListener(java.util.Locale);
method protected char[] getAcceptedChars();
method public int getInputType();
- method public static android.text.method.DateTimeKeyListener getInstance();
- field public static final char[] CHARACTERS;
+ method public static deprecated android.text.method.DateTimeKeyListener getInstance();
+ method public static android.text.method.DateTimeKeyListener getInstance(java.util.Locale);
+ field public static final deprecated char[] CHARACTERS;
}
public class DialerKeyListener extends android.text.method.NumberKeyListener {
@@ -41180,12 +41184,16 @@ package android.text.method {
}
public class DigitsKeyListener extends android.text.method.NumberKeyListener {
- ctor public DigitsKeyListener();
- ctor public DigitsKeyListener(boolean, boolean);
+ ctor public deprecated DigitsKeyListener();
+ ctor public deprecated DigitsKeyListener(boolean, boolean);
+ ctor public DigitsKeyListener(java.util.Locale);
+ ctor public DigitsKeyListener(java.util.Locale, boolean, boolean);
method protected char[] getAcceptedChars();
method public int getInputType();
- method public static android.text.method.DigitsKeyListener getInstance();
- method public static android.text.method.DigitsKeyListener getInstance(boolean, boolean);
+ method public static deprecated android.text.method.DigitsKeyListener getInstance();
+ method public static deprecated android.text.method.DigitsKeyListener getInstance(boolean, boolean);
+ method public static android.text.method.DigitsKeyListener getInstance(java.util.Locale);
+ method public static android.text.method.DigitsKeyListener getInstance(java.util.Locale, boolean, boolean);
method public static android.text.method.DigitsKeyListener getInstance(java.lang.String);
}
@@ -41329,11 +41337,13 @@ package android.text.method {
}
public class TimeKeyListener extends android.text.method.NumberKeyListener {
- ctor public TimeKeyListener();
+ ctor public deprecated TimeKeyListener();
+ ctor public TimeKeyListener(java.util.Locale);
method protected char[] getAcceptedChars();
method public int getInputType();
- method public static android.text.method.TimeKeyListener getInstance();
- field public static final char[] CHARACTERS;
+ method public static deprecated android.text.method.TimeKeyListener getInstance();
+ method public static android.text.method.TimeKeyListener getInstance(java.util.Locale);
+ field public static final deprecated char[] CHARACTERS;
}
public class Touch {
diff --git a/core/java/android/text/method/DateKeyListener.java b/core/java/android/text/method/DateKeyListener.java
index 88ef388bc420e..e14cd2cd725d0 100644
--- a/core/java/android/text/method/DateKeyListener.java
+++ b/core/java/android/text/method/DateKeyListener.java
@@ -16,9 +16,17 @@
package android.text.method;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.text.InputType;
import android.view.KeyEvent;
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.Locale;
+
/**
* For entering dates in a text field.
*
@@ -34,29 +42,76 @@ public class DateKeyListener extends NumberKeyListener
}
@Override
- protected char[] getAcceptedChars()
- {
- return CHARACTERS;
- }
-
- public static DateKeyListener getInstance() {
- if (sInstance != null)
- return sInstance;
-
- sInstance = new DateKeyListener();
- return sInstance;
+ @NonNull
+ protected char[] getAcceptedChars() {
+ return mCharacters;
}
/**
- * The characters that are used.
+ * @deprecated Use {@link #DateKeyListener(Locale)} instead.
+ */
+ @Deprecated
+ public DateKeyListener() {
+ this(null);
+ }
+
+ private static final String SYMBOLS_TO_IGNORE = "yMLd";
+ private static final String[] SKELETONS = {"yMd", "yM", "Md"};
+
+ public DateKeyListener(@Nullable Locale locale) {
+ final LinkedHashSet chars = new LinkedHashSet<>();
+ // First add the digits, then add all the non-pattern characters seen in the pattern for
+ // "yMd", which is supposed to only have numerical fields.
+ final boolean success = NumberKeyListener.addDigits(chars, locale)
+ && NumberKeyListener.addFormatCharsFromSkeletons(
+ chars, locale, SKELETONS, SYMBOLS_TO_IGNORE);
+ mCharacters = success ? NumberKeyListener.collectionToArray(chars) : CHARACTERS;
+ }
+
+ /**
+ * @deprecated Use {@link #getInstance(Locale)} instead.
+ */
+ @Deprecated
+ @NonNull
+ public static DateKeyListener getInstance() {
+ return getInstance(null);
+ }
+
+ /**
+ * Returns an instance of DateKeyListener appropriate for the given locale.
+ */
+ @NonNull
+ public static DateKeyListener getInstance(@Nullable Locale locale) {
+ DateKeyListener instance;
+ synchronized (sLock) {
+ instance = sInstanceCache.get(locale);
+ if (instance == null) {
+ instance = new DateKeyListener(locale);
+ sInstanceCache.put(locale, instance);
+ }
+ }
+ return instance;
+ }
+
+ /**
+ * This field used to list the characters that were used. But is now a fixed data
+ * field that is the list of code units used for the deprecated case where the class
+ * is instantiated with null or no input parameter.
*
* @see KeyEvent#getMatch
* @see #getAcceptedChars
+ *
+ * @deprecated Use {@link #getAcceptedChars()} instead.
*/
+ @Deprecated
public static final char[] CHARACTERS = new char[] {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'/', '-', '.'
};
- private static DateKeyListener sInstance;
+ private final char[] mCharacters;
+
+ private static final Object sLock = new Object();
+ @GuardedBy("sLock")
+ private static final HashMap sInstanceCache = new HashMap<>();
}
diff --git a/core/java/android/text/method/DateTimeKeyListener.java b/core/java/android/text/method/DateTimeKeyListener.java
index 523e98603a42a..62e3adea9b7a6 100644
--- a/core/java/android/text/method/DateTimeKeyListener.java
+++ b/core/java/android/text/method/DateTimeKeyListener.java
@@ -16,9 +16,17 @@
package android.text.method;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.text.InputType;
import android.view.KeyEvent;
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.Locale;
+
/**
* For entering dates and times in the same text field.
*
@@ -34,29 +42,80 @@ public class DateTimeKeyListener extends NumberKeyListener
}
@Override
+ @NonNull
protected char[] getAcceptedChars()
{
- return CHARACTERS;
- }
-
- public static DateTimeKeyListener getInstance() {
- if (sInstance != null)
- return sInstance;
-
- sInstance = new DateTimeKeyListener();
- return sInstance;
+ return mCharacters;
}
/**
- * The characters that are used.
+ * @deprecated Use {@link #DateTimeKeyListener(Locale)} instead.
+ */
+ @Deprecated
+ public DateTimeKeyListener() {
+ this(null);
+ }
+
+ private static final String SYMBOLS_TO_IGNORE = "yMLdahHKkms";
+ private static final String SKELETON_12HOUR = "yMdhms";
+ private static final String SKELETON_24HOUR = "yMdHms";
+
+ public DateTimeKeyListener(@Nullable Locale locale) {
+ final LinkedHashSet chars = new LinkedHashSet<>();
+ // First add the digits. Then, add all the character in AM and PM markers. Finally, add all
+ // the non-pattern characters seen in the patterns for "yMdhms" and "yMdHms".
+ boolean success = NumberKeyListener.addDigits(chars, locale)
+ && NumberKeyListener.addAmPmChars(chars, locale)
+ && NumberKeyListener.addFormatCharsFromSkeleton(
+ chars, locale, SKELETON_12HOUR, SYMBOLS_TO_IGNORE)
+ && NumberKeyListener.addFormatCharsFromSkeleton(
+ chars, locale, SKELETON_24HOUR, SYMBOLS_TO_IGNORE);
+ mCharacters = success ? NumberKeyListener.collectionToArray(chars) : CHARACTERS;
+ }
+
+ /**
+ * @deprecated Use {@link #getInstance(Locale)} instead.
+ */
+ @Deprecated
+ @NonNull
+ public static DateTimeKeyListener getInstance() {
+ return getInstance(null);
+ }
+
+ /**
+ * Returns an instance of DateTimeKeyListener appropriate for the given locale.
+ */
+ @NonNull
+ public static DateTimeKeyListener getInstance(@Nullable Locale locale) {
+ DateTimeKeyListener instance;
+ synchronized (sLock) {
+ instance = sInstanceCache.get(locale);
+ if (instance == null) {
+ instance = new DateTimeKeyListener(locale);
+ sInstanceCache.put(locale, instance);
+ }
+ }
+ return instance;
+ }
+
+ /**
+ * This field used to list the characters that were used. But is now a fixed data
+ * field that is the list of code units used for the deprecated case where the class
+ * is instantiated with null or no input parameter.
*
* @see KeyEvent#getMatch
* @see #getAcceptedChars
+ *
+ * @deprecated Use {@link #getAcceptedChars()} instead.
*/
public static final char[] CHARACTERS = new char[] {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'm',
'p', ':', '/', '-', ' '
};
- private static DateTimeKeyListener sInstance;
+ private final char[] mCharacters;
+
+ private static final Object sLock = new Object();
+ @GuardedBy("sLock")
+ private static final HashMap sInstanceCache = new HashMap<>();
}
diff --git a/core/java/android/text/method/DigitsKeyListener.java b/core/java/android/text/method/DigitsKeyListener.java
index 4aeb39a4876de..26c69ab01da00 100644
--- a/core/java/android/text/method/DigitsKeyListener.java
+++ b/core/java/android/text/method/DigitsKeyListener.java
@@ -16,11 +16,21 @@
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 java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.Locale;
/**
* For digits-only text entry
@@ -32,8 +42,20 @@ import android.view.KeyEvent;
public class DigitsKeyListener extends NumberKeyListener
{
private char[] mAccepted;
- private boolean mSign;
- private boolean mDecimal;
+ private final boolean mSign;
+ private final boolean mDecimal;
+
+ 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;
@@ -44,83 +66,218 @@ public class DigitsKeyListener extends NumberKeyListener
}
/**
- * The characters that are used.
+ * The characters that are used in compatibility mode.
*
* @see KeyEvent#getMatch
* @see #getAcceptedChars
*/
- private static final char[][] CHARACTERS = {
+ 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 static boolean isSignChar(final char c) {
- return c == '-' || c == '+';
+ private boolean isSignChar(final char c) {
+ return mSignChars.indexOf(c) != -1;
}
- // TODO: Needs internationalization
- private static boolean isDecimalPointChar(final char c) {
- return c == '.';
+ private boolean isDecimalPointChar(final char c) {
+ return mDecimalPointChars.indexOf(c) != -1;
}
/**
- * Allocates a DigitsKeyListener that accepts the digits 0 through 9.
+ * Allocates a DigitsKeyListener that accepts the ASCII digits 0 through 9.
+ *
+ * @deprecated Use {@link #DigitsKeyListener(Locale)} instead.
*/
+ @Deprecated
public DigitsKeyListener() {
- this(false, false);
+ this(null, false, false);
}
/**
- * Allocates a DigitsKeyListener that accepts the digits 0 through 9,
- * plus the minus sign (only at the beginning) and/or decimal point
+ * 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(boolean sign, boolean decimal) {
+ mDecimalPointChars = DEFAULT_DECIMAL_POINT_CHARS;
+ mSignChars = DEFAULT_SIGN_CHARS;
+ final int kind = (sign ? SIGN : 0) | (decimal ? DECIMAL : 0);
+ mAccepted = COMPATIBILITY_CHARACTERS[kind];
+ }
+
+ // 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;
+ if (locale == null) {
+ setToCompat(sign, decimal);
+ return;
+ }
+ LinkedHashSet chars = new LinkedHashSet<>();
+ final boolean success = NumberKeyListener.addDigits(chars, locale);
+ if (!success) {
+ setToCompat(sign, decimal);
+ 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(sign, decimal);
+ 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;
- int kind = (sign ? SIGN : 0) | (decimal ? DECIMAL : 0);
- mAccepted = CHARACTERS[kind];
+ 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(sign, decimal);
+ return;
+ }
+ final Character separatorChar = Character.valueOf(separatorString.charAt(0));
+ chars.add(separatorChar);
+ mDecimalPointChars = separatorChar.toString();
+ }
+ }
+ mAccepted = NumberKeyListener.collectionToArray(chars);
+ }
+
+ private DigitsKeyListener(@NonNull final String accepted) {
+ mSign = false;
+ mDecimal = false;
+ mAccepted = new char[accepted.length()];
+ accepted.getChars(0, accepted.length(), mAccepted, 0);
}
/**
- * Returns a DigitsKeyListener that accepts the digits 0 through 9.
+ * 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 digits 0 through 9,
- * plus the minus sign (only at the beginning) and/or decimal point
+ * 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) {
- int kind = (sign ? SIGN : 0) | (decimal ? DECIMAL : 0);
-
- if (sInstance[kind] != null)
- return sInstance[kind];
-
- sInstance[kind] = new DigitsKeyListener(sign, decimal);
- return sInstance[kind];
+ 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 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 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.
*/
- public static DigitsKeyListener getInstance(String accepted) {
- // TODO: do we need a cache of these to avoid allocating?
-
- DigitsKeyListener dim = new DigitsKeyListener();
-
- dim.mAccepted = new char[accepted.length()];
- accepted.getChars(0, accepted.length(), dim.mAccepted, 0);
-
- return dim;
+ @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;
}
public int getInputType() {
@@ -226,6 +383,4 @@ public class DigitsKeyListener extends NumberKeyListener
return null;
}
}
-
- private static DigitsKeyListener[] sInstance = new DigitsKeyListener[4];
}
diff --git a/core/java/android/text/method/NumberKeyListener.java b/core/java/android/text/method/NumberKeyListener.java
index 6b12b7e83c668..d40015ee17a82 100644
--- a/core/java/android/text/method/NumberKeyListener.java
+++ b/core/java/android/text/method/NumberKeyListener.java
@@ -16,15 +16,24 @@
package android.text.method;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.icu.text.DecimalFormatSymbols;
import android.text.Editable;
import android.text.InputFilter;
import android.text.Selection;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
+import android.text.format.DateFormat;
import android.view.KeyEvent;
import android.view.View;
+import libcore.icu.LocaleData;
+
+import java.util.Collection;
+import java.util.Locale;
+
/**
* For numeric text entry
*
@@ -38,6 +47,7 @@ public abstract class NumberKeyListener extends BaseKeyListener
/**
* You can say which characters you can accept.
*/
+ @NonNull
protected abstract char[] getAcceptedChars();
protected int lookup(KeyEvent event, Spannable content) {
@@ -137,4 +147,109 @@ public abstract class NumberKeyListener extends BaseKeyListener
adjustMetaAfterKeypress(content);
return super.onKeyDown(view, content, keyCode, event);
}
+
+ /* package */
+ @Nullable
+ static boolean addDigits(@NonNull Collection collection, @Nullable Locale locale) {
+ if (locale == null) {
+ return false;
+ }
+ final String[] digits = DecimalFormatSymbols.getInstance(locale).getDigitStrings();
+ for (int i = 0; i < 10; i++) {
+ if (digits[i].length() > 1) { // multi-codeunit digits. Not supported.
+ return false;
+ }
+ collection.add(Character.valueOf(digits[i].charAt(0)));
+ }
+ return true;
+ }
+
+ // From http://unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns
+ private static final String DATE_TIME_FORMAT_SYMBOLS =
+ "GyYuUrQqMLlwWdDFgEecabBhHKkjJCmsSAzZOvVXx";
+ private static final char SINGLE_QUOTE = '\'';
+
+ /* package */
+ static boolean addFormatCharsFromSkeleton(
+ @NonNull Collection collection, @Nullable Locale locale,
+ @NonNull String skeleton, @NonNull String symbolsToIgnore) {
+ if (locale == null) {
+ return false;
+ }
+ final String pattern = DateFormat.getBestDateTimePattern(locale, skeleton);
+ boolean outsideQuotes = true;
+ for (int i = 0; i < pattern.length(); i++) {
+ final char ch = pattern.charAt(i);
+ if (Character.isSurrogate(ch)) { // characters outside BMP are not supported.
+ return false;
+ } else if (ch == SINGLE_QUOTE) {
+ outsideQuotes = !outsideQuotes;
+ // Single quote characters should be considered if and only if they follow
+ // another single quote.
+ if (i == 0 || pattern.charAt(i - 1) != SINGLE_QUOTE) {
+ continue;
+ }
+ }
+
+ if (outsideQuotes) {
+ if (symbolsToIgnore.indexOf(ch) != -1) {
+ // Skip expected pattern characters.
+ continue;
+ } else if (DATE_TIME_FORMAT_SYMBOLS.indexOf(ch) != -1) {
+ // An unexpected symbols is seen. We've failed.
+ return false;
+ }
+ }
+ // If we are here, we are either inside quotes, or we have seen a non-pattern
+ // character outside quotes. So ch is a valid character in a date.
+ collection.add(Character.valueOf(ch));
+ }
+ return true;
+ }
+
+ /* package */
+ static boolean addFormatCharsFromSkeletons(
+ @NonNull Collection collection, @Nullable Locale locale,
+ @NonNull String[] skeletons, @NonNull String symbolsToIgnore) {
+ for (int i = 0; i < skeletons.length; i++) {
+ final boolean success = addFormatCharsFromSkeleton(
+ collection, locale, skeletons[i], symbolsToIgnore);
+ if (!success) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+
+ /* package */
+ static boolean addAmPmChars(@NonNull Collection collection,
+ @Nullable Locale locale) {
+ if (locale == null) {
+ return false;
+ }
+ final String[] amPm = LocaleData.get(locale).amPm;
+ for (int i = 0; i < amPm.length; i++) {
+ for (int j = 0; j < amPm[i].length(); j++) {
+ final char ch = amPm[i].charAt(j);
+ if (Character.isBmpCodePoint(ch)) {
+ collection.add(Character.valueOf(ch));
+ } else { // We don't support non-BMP characters.
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /* package */
+ @NonNull
+ static char[] collectionToArray(@NonNull Collection chars) {
+ final char[] result = new char[chars.size()];
+ int i = 0;
+ for (Character ch : chars) {
+ result[i++] = ch;
+ }
+ return result;
+ }
}
diff --git a/core/java/android/text/method/TimeKeyListener.java b/core/java/android/text/method/TimeKeyListener.java
index 01f40862ffb4c..c9f9f9fc5dde2 100644
--- a/core/java/android/text/method/TimeKeyListener.java
+++ b/core/java/android/text/method/TimeKeyListener.java
@@ -16,9 +16,17 @@
package android.text.method;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.text.InputType;
import android.view.KeyEvent;
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.Locale;
+
/**
* For entering times in a text field.
*
@@ -34,29 +42,80 @@ public class TimeKeyListener extends NumberKeyListener
}
@Override
+ @NonNull
protected char[] getAcceptedChars()
{
- return CHARACTERS;
- }
-
- public static TimeKeyListener getInstance() {
- if (sInstance != null)
- return sInstance;
-
- sInstance = new TimeKeyListener();
- return sInstance;
+ return mCharacters;
}
/**
- * The characters that are used.
+ * @deprecated Use {@link #TimeKeyListener(Locale)} instead.
+ */
+ @Deprecated
+ public TimeKeyListener() {
+ this(null);
+ }
+
+ private static final String SYMBOLS_TO_IGNORE = "ahHKkms";
+ private static final String SKELETON_12HOUR = "hms";
+ private static final String SKELETON_24HOUR = "Hms";
+
+ public TimeKeyListener(@Nullable Locale locale) {
+ final LinkedHashSet chars = new LinkedHashSet<>();
+ // First add the digits. Then, add all the character in AM and PM markers. Finally, add all
+ // the non-pattern characters seen in the patterns for "hms" and "Hms".
+ boolean success = NumberKeyListener.addDigits(chars, locale)
+ && NumberKeyListener.addAmPmChars(chars, locale)
+ && NumberKeyListener.addFormatCharsFromSkeleton(
+ chars, locale, SKELETON_12HOUR, SYMBOLS_TO_IGNORE)
+ && NumberKeyListener.addFormatCharsFromSkeleton(
+ chars, locale, SKELETON_24HOUR, SYMBOLS_TO_IGNORE);
+ mCharacters = success ? NumberKeyListener.collectionToArray(chars) : CHARACTERS;
+ }
+
+ /**
+ * @deprecated Use {@link #getInstance(Locale)} instead.
+ */
+ @Deprecated
+ @NonNull
+ public static TimeKeyListener getInstance() {
+ return getInstance(null);
+ }
+
+ /**
+ * Returns an instance of TimeKeyListener appropriate for the given locale.
+ */
+ @NonNull
+ public static TimeKeyListener getInstance(@Nullable Locale locale) {
+ TimeKeyListener instance;
+ synchronized (sLock) {
+ instance = sInstanceCache.get(locale);
+ if (instance == null) {
+ instance = new TimeKeyListener(locale);
+ sInstanceCache.put(locale, instance);
+ }
+ }
+ return instance;
+ }
+
+ /**
+ * This field used to list the characters that were used. But is now a fixed data
+ * field that is the list of code units used for the deprecated case where the class
+ * is instantiated with null or no input parameter.
*
* @see KeyEvent#getMatch
* @see #getAcceptedChars
+ *
+ * @deprecated Use {@link #getAcceptedChars()} instead.
*/
public static final char[] CHARACTERS = new char[] {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'm',
'p', ':'
};
- private static TimeKeyListener sInstance;
+ private final char[] mCharacters;
+
+ private static final Object sLock = new Object();
+ @GuardedBy("sLock")
+ private static final HashMap sInstanceCache = new HashMap<>();
}