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.text.DecimalFormatSymbols;
     22 import android.text.Editable;
     23 import android.text.InputFilter;
     24 import android.text.Selection;
     25 import android.text.Spannable;
     26 import android.text.SpannableStringBuilder;
     27 import android.text.Spanned;
     28 import android.text.format.DateFormat;
     29 import android.view.KeyEvent;
     30 import android.view.View;
     31 
     32 import libcore.icu.LocaleData;
     33 
     34 import java.util.Collection;
     35 import java.util.Locale;
     36 
     37 /**
     38  * For numeric text entry
     39  * <p></p>
     40  * As for all implementations of {@link KeyListener}, this class is only concerned
     41  * with hardware keyboards.  Software input methods have no obligation to trigger
     42  * the methods in this class.
     43  */
     44 public abstract class NumberKeyListener extends BaseKeyListener
     45     implements InputFilter
     46 {
     47     /**
     48      * You can say which characters you can accept.
     49      */
     50     @NonNull
     51     protected abstract char[] getAcceptedChars();
     52 
     53     protected int lookup(KeyEvent event, Spannable content) {
     54         return event.getMatch(getAcceptedChars(), getMetaState(content, event));
     55     }
     56 
     57     public CharSequence filter(CharSequence source, int start, int end,
     58                                Spanned dest, int dstart, int dend) {
     59         char[] accept = getAcceptedChars();
     60         boolean filter = false;
     61 
     62         int i;
     63         for (i = start; i < end; i++) {
     64             if (!ok(accept, source.charAt(i))) {
     65                 break;
     66             }
     67         }
     68 
     69         if (i == end) {
     70             // It was all OK.
     71             return null;
     72         }
     73 
     74         if (end - start == 1) {
     75             // It was not OK, and there is only one char, so nothing remains.
     76             return "";
     77         }
     78 
     79         SpannableStringBuilder filtered =
     80             new SpannableStringBuilder(source, start, end);
     81         i -= start;
     82         end -= start;
     83 
     84         int len = end - start;
     85         // Only count down to i because the chars before that were all OK.
     86         for (int j = end - 1; j >= i; j--) {
     87             if (!ok(accept, source.charAt(j))) {
     88                 filtered.delete(j, j + 1);
     89             }
     90         }
     91 
     92         return filtered;
     93     }
     94 
     95     protected static boolean ok(char[] accept, char c) {
     96         for (int i = accept.length - 1; i >= 0; i--) {
     97             if (accept[i] == c) {
     98                 return true;
     99             }
    100         }
    101 
    102         return false;
    103     }
    104 
    105     @Override
    106     public boolean onKeyDown(View view, Editable content,
    107                              int keyCode, KeyEvent event) {
    108         int selStart, selEnd;
    109 
    110         {
    111             int a = Selection.getSelectionStart(content);
    112             int b = Selection.getSelectionEnd(content);
    113 
    114             selStart = Math.min(a, b);
    115             selEnd = Math.max(a, b);
    116         }
    117 
    118         if (selStart < 0 || selEnd < 0) {
    119             selStart = selEnd = 0;
    120             Selection.setSelection(content, 0);
    121         }
    122 
    123         int i = event != null ? lookup(event, content) : 0;
    124         int repeatCount = event != null ? event.getRepeatCount() : 0;
    125         if (repeatCount == 0) {
    126             if (i != 0) {
    127                 if (selStart != selEnd) {
    128                     Selection.setSelection(content, selEnd);
    129                 }
    130 
    131                 content.replace(selStart, selEnd, String.valueOf((char) i));
    132 
    133                 adjustMetaAfterKeypress(content);
    134                 return true;
    135             }
    136         } else if (i == '0' && repeatCount == 1) {
    137             // Pretty hackish, it replaces the 0 with the +
    138 
    139             if (selStart == selEnd && selEnd > 0 &&
    140                     content.charAt(selStart - 1) == '0') {
    141                 content.replace(selStart - 1, selEnd, String.valueOf('+'));
    142                 adjustMetaAfterKeypress(content);
    143                 return true;
    144             }
    145         }
    146 
    147         adjustMetaAfterKeypress(content);
    148         return super.onKeyDown(view, content, keyCode, event);
    149     }
    150 
    151     /* package */
    152     @Nullable
    153     static boolean addDigits(@NonNull Collection<Character> collection, @Nullable Locale locale) {
    154         if (locale == null) {
    155             return false;
    156         }
    157         final String[] digits = DecimalFormatSymbols.getInstance(locale).getDigitStrings();
    158         for (int i = 0; i < 10; i++) {
    159             if (digits[i].length() > 1) { // multi-codeunit digits. Not supported.
    160                 return false;
    161             }
    162             collection.add(Character.valueOf(digits[i].charAt(0)));
    163         }
    164         return true;
    165     }
    166 
    167     // From http://unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns
    168     private static final String DATE_TIME_FORMAT_SYMBOLS =
    169             "GyYuUrQqMLlwWdDFgEecabBhHKkjJCmsSAzZOvVXx";
    170     private static final char SINGLE_QUOTE = '\'';
    171 
    172     /* package */
    173     static boolean addFormatCharsFromSkeleton(
    174             @NonNull Collection<Character> collection, @Nullable Locale locale,
    175             @NonNull String skeleton, @NonNull String symbolsToIgnore) {
    176         if (locale == null) {
    177             return false;
    178         }
    179         final String pattern = DateFormat.getBestDateTimePattern(locale, skeleton);
    180         boolean outsideQuotes = true;
    181         for (int i = 0; i < pattern.length(); i++) {
    182             final char ch = pattern.charAt(i);
    183             if (Character.isSurrogate(ch)) { // characters outside BMP are not supported.
    184                 return false;
    185             } else if (ch == SINGLE_QUOTE) {
    186                 outsideQuotes = !outsideQuotes;
    187                 // Single quote characters should be considered if and only if they follow
    188                 // another single quote.
    189                 if (i == 0 || pattern.charAt(i - 1) != SINGLE_QUOTE) {
    190                     continue;
    191                 }
    192             }
    193 
    194             if (outsideQuotes) {
    195                 if (symbolsToIgnore.indexOf(ch) != -1) {
    196                     // Skip expected pattern characters.
    197                     continue;
    198                 } else if (DATE_TIME_FORMAT_SYMBOLS.indexOf(ch) != -1) {
    199                     // An unexpected symbols is seen. We've failed.
    200                     return false;
    201                 }
    202             }
    203             // If we are here, we are either inside quotes, or we have seen a non-pattern
    204             // character outside quotes. So ch is a valid character in a date.
    205             collection.add(Character.valueOf(ch));
    206         }
    207         return true;
    208     }
    209 
    210     /* package */
    211     static boolean addFormatCharsFromSkeletons(
    212             @NonNull Collection<Character> collection, @Nullable Locale locale,
    213             @NonNull String[] skeletons, @NonNull String symbolsToIgnore) {
    214         for (int i = 0; i < skeletons.length; i++) {
    215             final boolean success = addFormatCharsFromSkeleton(
    216                     collection, locale, skeletons[i], symbolsToIgnore);
    217             if (!success) {
    218                 return false;
    219             }
    220         }
    221         return true;
    222     }
    223 
    224 
    225     /* package */
    226     static boolean addAmPmChars(@NonNull Collection<Character> collection,
    227                                 @Nullable Locale locale) {
    228         if (locale == null) {
    229             return false;
    230         }
    231         final String[] amPm = LocaleData.get(locale).amPm;
    232         for (int i = 0; i < amPm.length; i++) {
    233             for (int j = 0; j < amPm[i].length(); j++) {
    234                 final char ch = amPm[i].charAt(j);
    235                 if (Character.isBmpCodePoint(ch)) {
    236                     collection.add(Character.valueOf(ch));
    237                 } else {  // We don't support non-BMP characters.
    238                     return false;
    239                 }
    240             }
    241         }
    242         return true;
    243     }
    244 
    245     /* package */
    246     @NonNull
    247     static char[] collectionToArray(@NonNull Collection<Character> chars) {
    248         final char[] result = new char[chars.size()];
    249         int i = 0;
    250         for (Character ch : chars) {
    251             result[i++] = ch;
    252         }
    253         return result;
    254     }
    255 }
    256