Merge "Internationalize subclasses of NumberKeyListener"
This commit is contained in:
committed by
Android (Google) Code Review
commit
60bcd2485d
@@ -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.
|
||||
* <p></p>
|
||||
@@ -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<Character> 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<Locale, DateKeyListener> sInstanceCache = new HashMap<>();
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
* <p></p>
|
||||
@@ -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<Character> 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<Locale, DateTimeKeyListener> sInstanceCache = new HashMap<>();
|
||||
}
|
||||
|
||||
@@ -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<Character> 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<Locale, DigitsKeyListener[]> sLocaleInstanceCache =
|
||||
new HashMap<>();
|
||||
|
||||
/**
|
||||
* Returns a DigitsKeyListener that accepts the locale-appropriate digits, plus the
|
||||
* locale-appropriate plus or minus sign (only at the beginning) and/or the locale-appropriate
|
||||
* decimal separator (only one per field) if specified.
|
||||
*/
|
||||
@NonNull
|
||||
public static DigitsKeyListener getInstance(
|
||||
@Nullable Locale locale, boolean sign, boolean decimal) {
|
||||
final int kind = (sign ? SIGN : 0) | (decimal ? DECIMAL : 0);
|
||||
synchronized (sLocaleCacheLock) {
|
||||
DigitsKeyListener[] cachedValue = sLocaleInstanceCache.get(locale);
|
||||
if (cachedValue != null && cachedValue[kind] != null) {
|
||||
return cachedValue[kind];
|
||||
}
|
||||
if (cachedValue == null) {
|
||||
cachedValue = new DigitsKeyListener[4];
|
||||
sLocaleInstanceCache.put(locale, cachedValue);
|
||||
}
|
||||
return cachedValue[kind] = new DigitsKeyListener(locale, sign, decimal);
|
||||
}
|
||||
}
|
||||
|
||||
private static final Object sStringCacheLock = new Object();
|
||||
@GuardedBy("sStringCacheLock")
|
||||
private static final HashMap<String, DigitsKeyListener> sStringInstanceCache = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Returns a DigitsKeyListener that accepts only the characters
|
||||
* that appear in the specified String. Note that not all characters
|
||||
* may be available on every keyboard.
|
||||
*/
|
||||
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];
|
||||
}
|
||||
|
||||
@@ -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
|
||||
* <p></p>
|
||||
@@ -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<Character> 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<Character> 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<Character> 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<Character> 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<Character> chars) {
|
||||
final char[] result = new char[chars.size()];
|
||||
int i = 0;
|
||||
for (Character ch : chars) {
|
||||
result[i++] = ch;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
* <p></p>
|
||||
@@ -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<Character> 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<Locale, TimeKeyListener> sInstanceCache = new HashMap<>();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user