Home | History | Annotate | Download | only in method
      1 /*
      2  * Copyright (C) 2006 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.text.method;
     18 
     19 import android.annotation.NonNull;
     20 import android.annotation.Nullable;
     21 import android.icu.lang.UCharacter;
     22 import android.icu.lang.UProperty;
     23 import android.icu.text.DecimalFormatSymbols;
     24 import android.text.InputType;
     25 import android.text.SpannableStringBuilder;
     26 import android.text.Spanned;
     27 import android.view.KeyEvent;
     28 
     29 import com.android.internal.annotations.GuardedBy;
     30 import com.android.internal.util.ArrayUtils;
     31 
     32 import java.util.HashMap;
     33 import java.util.LinkedHashSet;
     34 import java.util.Locale;
     35 
     36 /**
     37  * For digits-only text entry
     38  * <p></p>
     39  * As for all implementations of {@link KeyListener}, this class is only concerned
     40  * with hardware keyboards.  Software input methods have no obligation to trigger
     41  * the methods in this class.
     42  */
     43 public class DigitsKeyListener extends NumberKeyListener
     44 {
     45     private char[] mAccepted;
     46     private boolean mNeedsAdvancedInput;
     47     private final boolean mSign;
     48     private final boolean mDecimal;
     49     private final boolean mStringMode;
     50     @Nullable
     51     private final Locale mLocale;
     52 
     53     private static final String DEFAULT_DECIMAL_POINT_CHARS = ".";
     54     private static final String DEFAULT_SIGN_CHARS = "-+";
     55 
     56     private static final char HYPHEN_MINUS = '-';
     57     // Various locales use this as minus sign
     58     private static final char MINUS_SIGN = '\u2212';
     59     // Slovenian uses this as minus sign (a bug?): http://unicode.org/cldr/trac/ticket/10050
     60     private static final char EN_DASH = '\u2013';
     61 
     62     private String mDecimalPointChars = DEFAULT_DECIMAL_POINT_CHARS;
     63     private String mSignChars = DEFAULT_SIGN_CHARS;
     64 
     65     private static final int SIGN = 1;
     66     private static final int DECIMAL = 2;
     67 
     68     @Override
     69     protected char[] getAcceptedChars() {
     70         return mAccepted;
     71     }
     72 
     73     /**
     74      * The characters that are used in compatibility mode.
     75      *
     76      * @see KeyEvent#getMatch
     77      * @see #getAcceptedChars
     78      */
     79     private static final char[][] COMPATIBILITY_CHARACTERS = {
     80         { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' },
     81         { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '+' },
     82         { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.' },
     83         { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '+', '.' },
     84     };
     85 
     86     private boolean isSignChar(final char c) {
     87         return mSignChars.indexOf(c) != -1;
     88     }
     89 
     90     private boolean isDecimalPointChar(final char c) {
     91         return mDecimalPointChars.indexOf(c) != -1;
     92     }
     93 
     94     /**
     95      * Allocates a DigitsKeyListener that accepts the ASCII digits 0 through 9.
     96      *
     97      * @deprecated Use {@link #DigitsKeyListener(Locale)} instead.
     98      */
     99     @Deprecated
    100     public DigitsKeyListener() {
    101         this(null, false, false);
    102     }
    103 
    104     /**
    105      * Allocates a DigitsKeyListener that accepts the ASCII digits 0 through 9, plus the ASCII plus
    106      * or minus sign (only at the beginning) and/or the ASCII period ('.') as the decimal point
    107      * (only one per field) if specified.
    108      *
    109      * @deprecated Use {@link #DigitsKeyListener(Locale, boolean, boolean)} instead.
    110      */
    111     @Deprecated
    112     public DigitsKeyListener(boolean sign, boolean decimal) {
    113         this(null, sign, decimal);
    114     }
    115 
    116     public DigitsKeyListener(@Nullable Locale locale) {
    117         this(locale, false, false);
    118     }
    119 
    120     private void setToCompat() {
    121         mDecimalPointChars = DEFAULT_DECIMAL_POINT_CHARS;
    122         mSignChars = DEFAULT_SIGN_CHARS;
    123         final int kind = (mSign ? SIGN : 0) | (mDecimal ? DECIMAL : 0);
    124         mAccepted = COMPATIBILITY_CHARACTERS[kind];
    125         mNeedsAdvancedInput = false;
    126     }
    127 
    128     private void calculateNeedForAdvancedInput() {
    129         final int kind = (mSign ? SIGN : 0) | (mDecimal ? DECIMAL : 0);
    130         mNeedsAdvancedInput = !ArrayUtils.containsAll(COMPATIBILITY_CHARACTERS[kind], mAccepted);
    131     }
    132 
    133     // Takes a sign string and strips off its bidi controls, if any.
    134     @NonNull
    135     private static String stripBidiControls(@NonNull String sign) {
    136         // For the sake of simplicity, we operate on code units, since all bidi controls are
    137         // in the BMP. We also expect the string to be very short (almost always 1 character), so we
    138         // don't need to use StringBuilder.
    139         String result = "";
    140         for (int i = 0; i < sign.length(); i++) {
    141             final char c = sign.charAt(i);
    142             if (!UCharacter.hasBinaryProperty(c, UProperty.BIDI_CONTROL)) {
    143                 if (result.isEmpty()) {
    144                     result = String.valueOf(c);
    145                 } else {
    146                     // This should happen very rarely, only if we have a multi-character sign,
    147                     // or a sign outside BMP.
    148                     result += c;
    149                 }
    150             }
    151         }
    152         return result;
    153     }
    154 
    155     public DigitsKeyListener(@Nullable Locale locale, boolean sign, boolean decimal) {
    156         mSign = sign;
    157         mDecimal = decimal;
    158         mStringMode = false;
    159         mLocale = locale;
    160         if (locale == null) {
    161             setToCompat();
    162             return;
    163         }
    164         LinkedHashSet<Character> chars = new LinkedHashSet<>();
    165         final boolean success = NumberKeyListener.addDigits(chars, locale);
    166         if (!success) {
    167             setToCompat();
    168             return;
    169         }
    170         if (sign || decimal) {
    171             final DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(locale);
    172             if (sign) {
    173                 final String minusString = stripBidiControls(symbols.getMinusSignString());
    174                 final String plusString = stripBidiControls(symbols.getPlusSignString());
    175                 if (minusString.length() > 1 || plusString.length() > 1) {
    176                     // non-BMP and multi-character signs are not supported.
    177                     setToCompat();
    178                     return;
    179                 }
    180                 final char minus = minusString.charAt(0);
    181                 final char plus = plusString.charAt(0);
    182                 chars.add(Character.valueOf(minus));
    183                 chars.add(Character.valueOf(plus));
    184                 mSignChars = "" + minus + plus;
    185 
    186                 if (minus == MINUS_SIGN || minus == EN_DASH) {
    187                     // If the minus sign is U+2212 MINUS SIGN or U+2013 EN DASH, we also need to
    188                     // accept the ASCII hyphen-minus.
    189                     chars.add(HYPHEN_MINUS);
    190                     mSignChars += HYPHEN_MINUS;
    191                 }
    192             }
    193             if (decimal) {
    194                 final String separatorString = symbols.getDecimalSeparatorString();
    195                 if (separatorString.length() > 1) {
    196                     // non-BMP and multi-character decimal separators are not supported.
    197                     setToCompat();
    198                     return;
    199                 }
    200                 final Character separatorChar = Character.valueOf(separatorString.charAt(0));
    201                 chars.add(separatorChar);
    202                 mDecimalPointChars = separatorChar.toString();
    203             }
    204         }
    205         mAccepted = NumberKeyListener.collectionToArray(chars);
    206         calculateNeedForAdvancedInput();
    207     }
    208 
    209     private DigitsKeyListener(@NonNull final String accepted) {
    210         mSign = false;
    211         mDecimal = false;
    212         mStringMode = true;
    213         mLocale = null;
    214         mAccepted = new char[accepted.length()];
    215         accepted.getChars(0, accepted.length(), mAccepted, 0);
    216         // Theoretically we may need advanced input, but for backward compatibility, we don't change
    217         // the input type.
    218         mNeedsAdvancedInput = false;
    219     }
    220 
    221     /**
    222      * Returns a DigitsKeyListener that accepts the ASCII digits 0 through 9.
    223      *
    224      * @deprecated Use {@link #getInstance(Locale)} instead.
    225      */
    226     @Deprecated
    227     @NonNull
    228     public static DigitsKeyListener getInstance() {
    229         return getInstance(false, false);
    230     }
    231 
    232     /**
    233      * Returns a DigitsKeyListener that accepts the ASCII digits 0 through 9, plus the ASCII plus
    234      * or minus sign (only at the beginning) and/or the ASCII period ('.') as the decimal point
    235      * (only one per field) if specified.
    236      *
    237      * @deprecated Use {@link #getInstance(Locale, boolean, boolean)} instead.
    238      */
    239     @Deprecated
    240     @NonNull
    241     public static DigitsKeyListener getInstance(boolean sign, boolean decimal) {
    242         return getInstance(null, sign, decimal);
    243     }
    244 
    245     /**
    246      * Returns a DigitsKeyListener that accepts the locale-appropriate digits.
    247      */
    248     @NonNull
    249     public static DigitsKeyListener getInstance(@Nullable Locale locale) {
    250         return getInstance(locale, false, false);
    251     }
    252 
    253     private static final Object sLocaleCacheLock = new Object();
    254     @GuardedBy("sLocaleCacheLock")
    255     private static final HashMap<Locale, DigitsKeyListener[]> sLocaleInstanceCache =
    256             new HashMap<>();
    257 
    258     /**
    259      * Returns a DigitsKeyListener that accepts the locale-appropriate digits, plus the
    260      * locale-appropriate plus or minus sign (only at the beginning) and/or the locale-appropriate
    261      * decimal separator (only one per field) if specified.
    262      */
    263     @NonNull
    264     public static DigitsKeyListener getInstance(
    265             @Nullable Locale locale, boolean sign, boolean decimal) {
    266         final int kind = (sign ? SIGN : 0) | (decimal ? DECIMAL : 0);
    267         synchronized (sLocaleCacheLock) {
    268             DigitsKeyListener[] cachedValue = sLocaleInstanceCache.get(locale);
    269             if (cachedValue != null && cachedValue[kind] != null) {
    270                 return cachedValue[kind];
    271             }
    272             if (cachedValue == null) {
    273                 cachedValue = new DigitsKeyListener[4];
    274                 sLocaleInstanceCache.put(locale, cachedValue);
    275             }
    276             return cachedValue[kind] = new DigitsKeyListener(locale, sign, decimal);
    277         }
    278     }
    279 
    280     private static final Object sStringCacheLock = new Object();
    281     @GuardedBy("sStringCacheLock")
    282     private static final HashMap<String, DigitsKeyListener> sStringInstanceCache = new HashMap<>();
    283 
    284     /**
    285      * Returns a DigitsKeyListener that accepts only the characters
    286      * that appear in the specified String.  Note that not all characters
    287      * may be available on every keyboard.
    288      */
    289     @NonNull
    290     public static DigitsKeyListener getInstance(@NonNull String accepted) {
    291         DigitsKeyListener result;
    292         synchronized (sStringCacheLock) {
    293             result = sStringInstanceCache.get(accepted);
    294             if (result == null) {
    295                 result = new DigitsKeyListener(accepted);
    296                 sStringInstanceCache.put(accepted, result);
    297             }
    298         }
    299         return result;
    300     }
    301 
    302     /**
    303      * Returns a DigitsKeyListener based on an the settings of a existing DigitsKeyListener, with
    304      * the locale modified.
    305      *
    306      * @hide
    307      */
    308     @NonNull
    309     public static DigitsKeyListener getInstance(
    310             @Nullable Locale locale,
    311             @NonNull DigitsKeyListener listener) {
    312         if (listener.mStringMode) {
    313             return listener; // string-mode DigitsKeyListeners have no locale.
    314         } else {
    315             return getInstance(locale, listener.mSign, listener.mDecimal);
    316         }
    317     }
    318 
    319     /**
    320      * Returns the input type for the listener.
    321      */
    322     public int getInputType() {
    323         int contentType;
    324         if (mNeedsAdvancedInput) {
    325             contentType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_NORMAL;
    326         } else {
    327             contentType = InputType.TYPE_CLASS_NUMBER;
    328             if (mSign) {
    329                 contentType |= InputType.TYPE_NUMBER_FLAG_SIGNED;
    330             }
    331             if (mDecimal) {
    332                 contentType |= InputType.TYPE_NUMBER_FLAG_DECIMAL;
    333             }
    334         }
    335         return contentType;
    336     }
    337 
    338     @Override
    339     public CharSequence filter(CharSequence source, int start, int end,
    340                                Spanned dest, int dstart, int dend) {
    341         CharSequence out = super.filter(source, start, end, dest, dstart, dend);
    342 
    343         if (mSign == false && mDecimal == false) {
    344             return out;
    345         }
    346 
    347         if (out != null) {
    348             source = out;
    349             start = 0;
    350             end = out.length();
    351         }
    352 
    353         int sign = -1;
    354         int decimal = -1;
    355         int dlen = dest.length();
    356 
    357         /*
    358          * Find out if the existing text has a sign or decimal point characters.
    359          */
    360 
    361         for (int i = 0; i < dstart; i++) {
    362             char c = dest.charAt(i);
    363 
    364             if (isSignChar(c)) {
    365                 sign = i;
    366             } else if (isDecimalPointChar(c)) {
    367                 decimal = i;
    368             }
    369         }
    370         for (int i = dend; i < dlen; i++) {
    371             char c = dest.charAt(i);
    372 
    373             if (isSignChar(c)) {
    374                 return "";    // Nothing can be inserted in front of a sign character.
    375             } else if (isDecimalPointChar(c)) {
    376                 decimal = i;
    377             }
    378         }
    379 
    380         /*
    381          * If it does, we must strip them out from the source.
    382          * In addition, a sign character must be the very first character,
    383          * and nothing can be inserted before an existing sign character.
    384          * Go in reverse order so the offsets are stable.
    385          */
    386 
    387         SpannableStringBuilder stripped = null;
    388 
    389         for (int i = end - 1; i >= start; i--) {
    390             char c = source.charAt(i);
    391             boolean strip = false;
    392 
    393             if (isSignChar(c)) {
    394                 if (i != start || dstart != 0) {
    395                     strip = true;
    396                 } else if (sign >= 0) {
    397                     strip = true;
    398                 } else {
    399                     sign = i;
    400                 }
    401             } else if (isDecimalPointChar(c)) {
    402                 if (decimal >= 0) {
    403                     strip = true;
    404                 } else {
    405                     decimal = i;
    406                 }
    407             }
    408 
    409             if (strip) {
    410                 if (end == start + 1) {
    411                     return "";  // Only one character, and it was stripped.
    412                 }
    413 
    414                 if (stripped == null) {
    415                     stripped = new SpannableStringBuilder(source, start, end);
    416                 }
    417 
    418                 stripped.delete(i - start, i + 1 - start);
    419             }
    420         }
    421 
    422         if (stripped != null) {
    423             return stripped;
    424         } else if (out != null) {
    425             return out;
    426         } else {
    427             return null;
    428         }
    429     }
    430 }
    431