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<>(); }