From 3484ba8fdc8f5c91937af23e6d59025081c02367 Mon Sep 17 00:00:00 2001 From: Roozbeh Pournader Date: Thu, 19 Jan 2017 14:05:51 -0800 Subject: [PATCH] Internationalize subclasses of NumberKeyListener The previous implementation assumed ASCII digits, and a fixed set of separators and signs. The following classes have been internationalized: DigitsKeyListener, DateKeyListener, TimeKeyListener, and DateTimeKeyListener. DialerKeyListener is not modified yet, due to concerns about the handling of non-ASCII digits by dialing-related parts of Android. Even with this CL, characters outside BMP and multi-character signs and decimal separators are still not supported, due to limitations in android.view.KeyEvent and android.view.KeyCharacterMap. Test: cts-tradefed run cts-dev --module CtsTextTestCases --test android.text.method.cts.*KeyListenerTest Bug: https://code.google.com/p/android/issues/detail?id=2626 Bug: https://code.google.com/p/android/issues/detail?id=82993 Bug: 8319249 Bug: 33276673 Bug: 33643035 Bug: 34394455 Change-Id: I1cf87d0d9d1b383f5265c07ecd63b5767f9a68ca --- api/current.txt | 36 ++- api/system-current.txt | 36 ++- api/test-current.txt | 36 ++- .../android/text/method/DateKeyListener.java | 81 ++++++- .../text/method/DateTimeKeyListener.java | 81 ++++++- .../text/method/DigitsKeyListener.java | 227 +++++++++++++++--- .../text/method/NumberKeyListener.java | 115 +++++++++ .../android/text/method/TimeKeyListener.java | 81 ++++++- 8 files changed, 583 insertions(+), 110 deletions(-) diff --git a/api/current.txt b/api/current.txt index 75d648e7049cd..843fb8349e780 100644 --- a/api/current.txt +++ b/api/current.txt @@ -40008,19 +40008,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 { @@ -40032,12 +40036,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); } @@ -40181,11 +40189,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 1fbad0e212bcf..dd6174ed27bdf 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -43264,19 +43264,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 { @@ -43288,12 +43292,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); } @@ -43437,11 +43445,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 8f2389281663a..457662a230037 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -40130,19 +40130,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 { @@ -40154,12 +40158,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); } @@ -40303,11 +40311,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<>(); }